ADR-0005. UoM storage — uom jsonb on catalog + uomId soft ref on lines
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-04-05 |
| Deciders | inventory-team, PM |
| Supersedes | — |
Context
Materialhas multiple UoM "roles": base (storage unit, e.g. gram), purchase (vendor invoice unit, e.g. kg), sale (display unit, e.g. portion).PurchaseOrderItem,VendorItem,MaterialRecipeItem,InventoryTrackingeach have a single UoM context per row.- Hard FK from line tables to
UnitOfMeasureis fragile: UoM rows are reference data and may be soft-deleted or re-keyed by merchants.
Decision
Two-layer storage:
- Catalog entities (
Material,ProductVariant) store roles in a single jsonb columnuomof shape:tstype TUomRoles = { base: { id: string; code: string; name: I18n; ratio: number }; purchase?: { ... }; sale?: { ... }; }; - Line entities (
PurchaseOrderItem,VendorItem,MaterialRecipeItem,InventoryTracking) storeuomId: textas a soft reference plusmultiplier: decimal(15,4). Themodel.tsrelations()declaration provides theuomaccessor for queries; the DB has no foreign key.
Consequences
| Pros | Cons |
|---|---|
Catalog snapshots are stable — even if a merchant's UoM is deleted, Material.uom survives | Need service-layer validation that uomId resolves at write time |
Line entities decouple from UnitOfMeasure lifecycle | No DB-level referential integrity for uomId |
multiplier is captured at line-write time → unit conversion immutable post-fact | Two storage shapes to remember (jsonb vs uomId) |
Replay/audit reasoning works without joining UnitOfMeasure | Reports must be careful to use line-level multiplier, not catalog uom |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
Hard FK uomId → UnitOfMeasure.id everywhere | Referential integrity | Breaks on UoM soft-delete; fragile under merchant overrides | Wrong primitive for reference data |
| Single jsonb on every line entity | No FK fragility | Repeats UoM data per line; hard to query "all lines with kg" | Storage waste, query pain |
UnitOfMeasure with versioned rows (no soft-delete) | Stable IDs | Massive design change, doesn't fix multiplier-snapshot need | Out of scope |
References
core/src/models/schemas/inventory/material/schema.ts(uomjsonb)core/src/models/schemas/inventory/purchase-order-item/schema.ts(uomIdsoft ref +model.tsuomrelation)- Memory:
feedback_uom_storage_convention.md