What you have today
The platform follows the regulatory split described in Data Model.md §1.1 and docs/manufacturer-flows.md: enter static, family-level data once per battery model, then register one passport per physical unit with serial identity and dynamic state.
This matches DIN / DKE practice (model code + serialised units) and the existing
BatteryModel and Passport entities in the database.
Two layers at a glance
Enter once per product family
attribute_value
holder = MODEL
Per physical battery
attribute_value
holder = PASSPORT
Shared vendor specs (materials, circularity, primary BOM) live on the model; passports carry unit identity, optional overrides, lifecycle state, and time-series readings.
Core entities
| Entity | Purpose | Typical identifiers |
|---|---|---|
organization |
Tenant workspace (manufacturer, repairer, supplier, …). All customer data is scoped by tenant_id. |
Public org id, legal name |
battery_model |
Product family / type approval unit. Shared regulatory static data for every passport of this model. | public_model_code, optional reference_code from LMT catalogue |
passport |
One row per manufactured battery unit. Links to exactly one model. | public_id (QR), serial_number |
attribute_definition |
Catalogue of ~95 DIN fields (and future delegated acts). Metadata drives UI, validation, and compliance. | attribute_key (e.g. circ.recycled_content_pct) |
attribute_value |
Actual field values, bound polymorphically to MODEL or PASSPORT (and other holders in later phases). | Holder kind + holder UUID |
bom / bom_line |
Bill of materials. Primary BOM is on the model; legacy per-passport BOM rows still resolve as fallback. | Model-scoped mass balance |
Attribute-as-data (not columns per field)
Regulatory attributes are not individual columns on
passport or battery_model. Each field is defined in
attribute_definition and stored in attribute_value rows.
Adding a new DIN field is a catalogue migration, not a DDL change.
Catalogue flags control where values live and how passports read them:
is_model_level- Values may only be authored on the MODEL holder. Passports never store a separate row for this key.
inherits_from_model- Passport UI and compliance use the effective value: model value unless the unit has an explicit PASSPORT override.
Examples of model-level / inherited fields in Phase 1: recycled content %, recovery targets, repairability index, hazardous substances declaration, cathode/anode chemistry where shared across units.
LMT reference catalogue
The LMT reference catalogue is tenant-agnostic reference data (28 demo e-bike packs locally).
A model may optionally provision from a catalogue entry via reference_code when creating
or importing a model — copying baseline attributes without re-keying the datasheet.
In the app: LMT reference catalogue
(manufacturer workspace). Model CSV import can set reference_code to link a row to catalogue data.
Carbon, BOM, and JSON metadata
- Carbon footprint — declared on the model (
metadata_json.carbon): class, kg CO₂e/kWh, lifecycle stages. Shown on passport detail by read-through from the model. - Primary BOM — stored with
battery_model_idand nullpassport_id. Passport BOM screens redirect to model BOM editing; mass balance for compliance resolves model BOM first. - Passport static draft — production date on the entity; plant and notes in
metadata_json.staticuntil full version locking (Project 1.1).
Dynamic and time-series data
State of health, cycle counts, and BMS readings are per passport (and eventually TimescaleDB hypertables in production). They do not belong on the model row.
The REST ingest API (/api/v1/...) and manufacturer performance views attach readings
to the passport identity, not the model family.
Effective values in the application
When you open a passport’s Materials or Circularity tab, the UI shows effective attributes: inherited from the model plus any unit-specific override. Inherited fields display a banner and link to Edit on model rather than duplicating vendor specs on every serial number.
Completeness and fleet compliance use the same resolution — a passport is not marked incomplete for recycled content that is already declared on its model.
Where to work in the application
| What you are editing | Holder / entity | Route in this app |
|---|---|---|
| Register product family | BatteryModel |
/app/models/new |
| Import vendor datasheet (CSV) | MODEL attributes + model row | /app/models → Import CSV |
| Materials & composition (shared) | MODEL attribute_value |
/app/models/{id}/materials/edit |
| Circularity & recycled content (shared) | MODEL attribute_value |
/app/models/{id}/circularity/edit |
| Bill of materials (primary) | MODEL bom |
/app/models/{id}/bom |
| Carbon declaration | Model metadata_json |
Model detail / carbon form (from models list) |
| Register physical unit | Passport |
/app/passports/new |
| Bulk serial units (CSV) | Passport rows only | /app/passports → Import (columns: public_id, model_code, serial_number) |
| Passport detail (read inherited + unit data) | Effective MODEL ∪ PASSPORT | /app/passports/{id} |
| Unit static data override | PASSPORT | /app/passports/{id}/edit |
| Public QR / Art. 77 surface | Passport public_id |
/p/{publicId} (anonymous) |
| LMT catalogue browse | Reference data | /app/catalog/lmt-batteries |
Routes under /app/... require a signed-in manufacturer (or appropriate partner) session.
Help pages are public at /help/**.
Recommended manufacturer workflow
- Create or import a battery model (and optionally link LMT reference code).
- Complete model-level materials, circularity, carbon, and BOM once.
- Create passports (form or CSV) with serial numbers pointing at that model code.
- Fill unit-specific fields and dynamic data per passport; use passport detail for lifecycle, NB audit, and market placement.
Further reading
- Data Model.md — full entity domains, §6 attribute tables, versioning, multi-tenant rules.
- docs/manufacturer-flows.md — Phase 1 workflow decisions (WF-01 … WF-13).
- docs/din-attribute-catalog.md — catalogue CSV columns including
is_model_levelandinherits_from_model.