Skip to content

PRD: Unit of measure

ModuleInventory (CORE-06)PRD IDPRD-ITM-001
StatusShippedOwnerInventory squad
Date2026-05-04Versionv1.0
Packages@nx/inventory · @nx/core · apps/clientURDITM · POI

TL;DR

Gives a merchant a managed catalog of units of measure - "kg", "unit", "dozen" and their own custom units - so every inventory item and purchase-order line references a real, shared unit instead of an implicit string. Units carry a category, a conversion ratio and a base-unit chain, ship seeded with system defaults, and are editable from a client screen. The result: the unit dimension every catalog entity runs on is finally explicit, consistent, and merchant-owned.

1. Context & Problem

Inventory items are keyed by (variant, location, unit) (URD-ITM-002) and purchase-order lines accumulate by (itemType, itemId, uom) (URD-POI-001) - yet there is no managed catalog of those units. Codes like "kg", "unit" or "dozen" are implicit strings with no shared conversion ratios, no merchant-specific custom units, and no UI for a merchant to see or edit the units their catalog runs on. The Definitions §3 entry already defines a Unit of Measure as a "measurement unit with 3-level scope: system defaults → merchant → product/material," but nothing realizes it.

This makes the unit dimension fragile: two items can drift on what "box" means, ratios are duplicated everywhere they are needed, and a merchant cannot introduce units specific to their trade. A first-class Unit of Measure catalog is the prerequisite for consistent stock math, PO line accumulation, and (later) recipe/BOM unit handling.

2. Goals & Non-Goals

Goals

  • A two-tier UoM catalog: seeded system defaults (merchantId = NULL) plus merchant-scoped custom units; resolution is merchant → system, first match per code wins.
  • Each unit carries an i18n name/description, a grouping category (count, weight, volume, length, time, area, other), a conversion ratio, and a self-referencing base unit (referenceId) so ratios chain instead of being duplicated.
  • A full unit lifecycle (ACTIVATED → DEACTIVATED → ARCHIVED), per-merchant isolation, soft-delete, and a unique (code, merchantId) partial index.
  • A CRUD API for units, with the seed pre-populating the default catalog at startup.
  • A client UoM management screen: create, edit and list views, a category picker, base-unit selection, and schema validation.

Non-Goals

  • Cross-category conversion (weight ↔ volume) - categories exist precisely to block nonsensical conversions at the application layer, not to bridge them.
  • A third product-/material-specific UoM tier - product pack-size overrides live on the product entity via base/purchase/sale UoM roles, not as a UoM row.
  • Recipe/BOM unit handling - owned by the in-progress Recipes / BOM area.
  • The back-office (apps/bo) UoM page - a later, separate surface.

3. Success Metrics

MetricTarget / signal
Unit consistency100% of inventory items and PO lines reference a catalog unit (not an ad-hoc string)
Seed coverageEvery merchant resolves the full system-default unit catalog at startup with zero gaps
Custom adoptionMerchants in unit-heavy trades create at least one custom unit
Conversion integrityZero cross-category conversions accepted; base-unit chains resolve without cycles

4. Personas & Use Cases

PersonaGoal in this feature
OwnerDefine the units their catalog runs on, including units specific to their trade
Inventory staffPick the right unit when creating items and PO lines; trust that "kg" means one thing
SystemSeed the default unit catalog so every merchant starts with a usable set

Core scenarios: a merchant opens the UoM screen → sees the seeded system defaults → adds a custom unit with a category, a conversion ratio and a base unit → edits or deactivates units as their catalog evolves → every inventory item and PO line picks units from this catalog.

5. User Stories

  • As an owner, I want a starting set of common units to already exist, so that I can use units without configuring anything first.
  • As an owner, I want to add a custom unit specific to my trade, so that my catalog speaks my units, not just the defaults.
  • As an owner, I want each unit to belong to a category and chain to a base unit by a ratio, so that conversions are shared instead of re-entered everywhere.
  • As inventory staff, I want to pick a unit from a managed list when creating items and PO lines, so that the unit dimension is consistent across the catalog.
  • As an owner, I want to deactivate or archive a unit I no longer use, so that stale units stop appearing while history is preserved.

6. Functional Requirements

#RequirementURD ref
FR-1A Unit of Measure entity with i18n name/description, category, status, conversion ratio, self-ref referenceId, and merchantIdURD-ITM-002 · Definitions §3
FR-2Two-tier scoping: seeded system defaults (merchantId = NULL) + merchant-scoped units; resolution merchant → system, first match per code winsDefinitions §3
FR-3Unique partial index on (code, merchantId) where not deleted, so a code is unique within a merchant's scopeURD-ITM-002
FR-4Category taxonomy (count / weight / volume / length / time / area / other) with i18n labels; categories block cross-category conversionDefinitions §3
FR-5Self-referencing base unit (referenceId) with a conversion ratio so ratios chain instead of being duplicatedDefinitions §3
FR-6Unit lifecycle ACTIVATED → DEACTIVATED → ARCHIVED, per-merchant isolation, and soft-deleteURD-ITM-005
FR-7A CRUD API for units (create / update / list), permission-gated under the unit-of-measures subjectURD-ITM-002
FR-8A seed pre-populates the system-default unit catalog at startupDefinitions §3
FR-9UoM roles (base / purchase / sale) bind catalog entities to units; PO lines and items default to the item's base unitURD-POI-005
FR-10A client UoM management screen: create, edit, list, a category picker, base-unit selection, and schema validationURD-ITM-002

Full requirement text and acceptance criteria live in the Inventory URD. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
Data integrityA code is unique within a merchant's scope via the (code, merchantId) partial index; base-unit chains must not form cycles
Tenancy & authzMerchant-scoped (x-merchant-id); system defaults shared read-only; mutations gated by the unit-of-measures permission subject
ResolutionTwo-tier lookup resolves merchant → system, first match per code wins - deterministic per merchant
Soft-deleteUnits use soft-delete; deactivated/archived/deleted units are preserved and excluded from standard listings
i18nUnit names, descriptions and category labels are bilingual ({ en, vi })
PrecisionConversion ratios use float(value, 4)

8. UX & Flows

Key screens (in apps/client): the UoM list view, create/edit screens with a category picker and a base-unit selection component, all backed by schema validation.

9. Data & Domain

EntityRole
UnitOfMeasureA unit row - i18n name/description, category, status, conversion ratio, self-ref referenceId, merchantId (NULL for system defaults)
UoM categoryGrouping taxonomy (count / weight / volume / length / time / area / other) with i18n labels; bounds valid conversions
UoM role (base / purchase / sale)Binds a catalog entity (variant/material) to its units; PO lines default to the base unit
Seed migrationPre-populates the system-default unit catalog at startup

Conceptual only - full schema and invariants in the inventory domain model and ADR-0005 UoM two-layer.

10. Dependencies & Assumptions

Depends on

  • Inventory Items (URD-ITM) - items are keyed by (variant, location, unit); the catalog supplies that unit.
  • Purchase Order Items (URD-POI) - PO lines accumulate per uom and default to the item's base unit.
  • @nx/core - owns the UnitOfMeasure schema, category constants, UoM-role interface, and the scoped repository.

Assumptions

  • The system-default unit catalog is seeded before any merchant resolves units.
  • Categories are a fixed, application-level taxonomy; cross-category conversion is intentionally impossible.
  • Product/material pack-size overrides are expressed through UoM roles on the variant, not as extra catalog tiers.

11. Risks & Open Questions

Risk / questionMitigation / status
A merchant code could collide with a system defaultResolution is merchant → system, first match per code wins; the merchant unit shadows the default deterministically
Base-unit chains could form a cycleSelf-ref referenceId must point to an existing unit; chains are validated to terminate at a base unit
Cross-category conversion expectationsOut of scope by design - categories block it; document as a constraint
Editing a unit referenced by live items/linesUnits use soft-delete and a status lifecycle; deactivate rather than hard-delete to preserve references

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (foundation) - supports the ITM feature in the URD feature catalog
RolloutAll merchants; no feature flag
MigrationSystem-default unit catalog seeded at startup; no per-merchant data migration
Launch criteriaSeed resolves the full default catalog; create/edit/list verified end-to-end; unique (code, merchantId) enforced; cross-category conversion rejected
MonitoringCustom-unit creation per merchant, seed-resolution completeness, conversion-rejection rate

13. FAQ

Where do the starting units come from? A seed pre-populates a system-default catalog (merchantId = NULL) at startup, so every merchant has a usable set without configuring anything.

How does a custom unit relate to a system default with the same code? Resolution is merchant → system, first match per code wins - a merchant unit shadows the system default for that merchant.

Can I convert kilograms to litres? No - categories (weight, volume, …) exist precisely to block cross-category conversion. Conversion only chains within a category via the base-unit ratio.

Is there a third product-specific unit tier? No - product/material pack-size is expressed through base/purchase/sale UoM roles on the variant, not as an extra UoM catalog row.

Where is the UoM screen? In apps/client - a list view plus create/edit screens with a category picker and base-unit selection. The back-office page is a separate, later surface.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.