Skip to content

Domain Model

All schemas defined in @nx/core/src/models/schemas/inventory/. Tables use PascalCase names. Numeric columns use standardNumeric = decimal(15, 4).

1. Full ERD

2. Common Columns

Every entity below adds these via generateCommonColumnDefs() unless noted.

ColumnTypeNotes
idtextPK, Snowflake via IdGenerator
createdAttimestamptznow() default
modifiedAttimestamptzupdated on write
createdBytextuser id
modifiedBytextuser id
deletedAttimestamptzsoft-delete marker
metadatajsonbextension bag
statustextwhen using generateCommonColumnWithStatusDefs

3. Entities

3.1 InventoryLocation

PropertyValue
TableInventoryLocation
Sourcecore/src/models/schemas/inventory/inventory-location/schema.ts
Soft-deleteyes
OwnermerchantId
FieldTypeRequiredDescription
merchantIdtextOwner
parentIdtextSelf-ref for hierarchy
identifiertextAuto, LOC prefix
isDefaultbooleanDefault false; partial unique per merchant when true
statustextNEW / ACTIVATED / DEACTIVATED / ARCHIVED (default NEW)
namei18n jsonbDisplay name
typetextInventoryLocationTypes (default PHYSICAL)
locationjsonbAddress ({ main, sub, long, lat, postCode })

Invariants: exactly one isDefault=true per merchant; cycle-safe parentId.


3.2 InventoryItem

PropertyValue
TableInventoryItem
Polymorphicyes — (itemType, itemId) via generatePrincipalColumnDefs({ discriminator: 'item' }); references Material or ProductVariant
FieldTypeRequiredDescription
merchantIdtextOwner
itemTypetextMATERIAL / PRODUCT_VARIANT
itemIdtextFK target id
identifiertextAuto-generated, INI prefix
statustextInventoryItemStatuses (default ACTIVATED)

Indexes: partial unique (merchantId, itemType, itemId) WHERE deletedAt IS NULL; non-unique (merchantId), (merchantId, status). Idempotent upsert: InventoryItemRepository.ensureInventoryItem keyed by (merchantId, itemType, itemId).


3.3 InventoryStock

PropertyValue
TableInventoryStock
Sourcecore/src/models/schemas/inventory/inventory-stock/schema.ts
Bucket key(inventoryItemId, inventoryLocationId, lotNumber, serialNumber) UNIQUE NULLS NOT DISTINCT
FieldTypeRequiredDescription
inventoryItemIdtextFK
inventoryLocationIdtextFK
merchantIdtextDenormalized from InventoryItem
quantityOnHanddecimal(15,4)Total physical stock
quantityReserveddecimal(15,4)Allocated for orders
quantityAvailabledecimal(15,4)Stored field; service maintains as onHand − reserved
lastCountedAttimestamptz
lastStockedAttimestamptz
lotNumbertextBucket-key extension
serialNumbertextBucket-key extension
expiryDatetimestamptzFEFO support
manufactureDatetimestamptz
averageCostdecimal(15,4)AVCO snapshot
costingMethodtextAVERAGE (default), FIFO, LIFO, etc.

Indexes: (inventoryItemId), (inventoryLocationId), (merchantId).

Atomic mutator: InventoryStockRepository.adjustStock({ stockId, adjustOnHand, adjustAvailable, adjustReserved, forceNonNegative }) — single SQL UPDATE with optional non-negative guard. Returns null if guard fails.


3.4 InventoryTracking

PropertyValue
TableInventoryTracking
Mutabilityappend-only audit log; CRUD repository but service writes only on stock change
FieldTypeRequiredDescription
inventoryStockIdtextFK
merchantIdtextDenormalized from stock chain
referenceTypetextOne of §4; typed<TInventoryTrackingReferenceType>
referenceIdtextOriginating doc id (nullable for orphan adjustments)
uomIdtextUnit at write time
multiplierdecimal(15,4)UoM conversion factor (default 1)
quantityBeforedecimal(15,4)Pre-mutation snapshot
quantityChangedecimal(15,4)Delta (signed)
quantityAfterdecimal(15,4)Post-mutation snapshot
effectivePricedecimal(15,4)Per-unit cost (PURCHASE writes only)
fromLocationIdtextTRANSFER source
toLocationIdtextTRANSFER destination
reasonCodetextOne of 13 InventoryTrackingReasons
lotNumber / serialNumber / expiryDatetext / timestamptzAudit-immutable snapshot of moved bucket
remainingQuantitydecimal(15,4)FIFO layer tracker (decrements as outbound consumes)
notetextFree-form (see InventoryTrackingNotes)
createdBy / modifiedBytextUser audit (anonymous allowed)

Indexes: (inventoryStockId), (referenceId), (referenceType, referenceId), (fromLocationId), (toLocationId), (uomId), (merchantId).

Idempotency: lookups by (referenceType, referenceId, inventoryStockId) before write to avoid double-count on Kafka redelivery.


3.5 InventoryIdentifier

PropertyValue
TableInventoryIdentifier
Polymorphictags InventoryItem or InventoryStock
FieldTypeRequiredDescription
principalTypetextINVENTORY_ITEM / INVENTORY_STOCK
principalIdtextFK target id
schemetextSKU / BARCODE / QRCODE / IMEI / SERIAL
valuetextIdentifier string

Constraint: unique (scheme, value) per principal — prevents duplicate barcodes.


3.6 InventoryTicket / InventoryTicketItem

InventoryTicket fields:

FieldTypeRequiredDescription
merchantIdtextOwner
identifiertextAuto, ITI prefix
typetextInventoryTicketTypes (default UNKNOWN); see §5.4 for full set
statustextDefault DRAFT
partnerType / partnerIdtextVENDOR / CUSTOMER (when applicable)
sourceLocationId / destinationLocationIdtextFor TRANSFER
originReferenceType / originReferenceIdtextLineage to triggering doc (e.g., a sale return ref)
returnOfTicketIdtextFK self — when this ticket is a return of another
backorderOfTicketIdtextFK self — backorder lineage
spawnedPurchaseOrderIdtextFK to PurchaseOrder — when ticket spawned a PO
reasonCodetextOne of InventoryTrackingReasons
effectiveDate / submittedAt / approvedAt / startedAt / completedAt / cancelledAttimestamptzPer-status timestamps
approvedBytextUser audit for approval step
notetextFree-form

Behavior: ticket is a workflow document — no stock effect until COMPLETED (at which point InventoryTicketItem lines drive stock adjustments).


3.7 PurchaseOrder / PurchaseOrderItem

PurchaseOrder fields:

FieldTypeRequiredDescription
merchantIdtextOwner
purchaseOrderNumbertextUnique; default = <YYYYMMDDHHmmss>-<snowflake>
nametextDefault = PurchaseOrder-<snowflake>
slugtextUnique; same default pattern
vendorIdtextFK to Vendor
inventoryLocationIdtextDestination on receive
statustextSee §5.1 — 6 values, default DRAFT
orderDatetimestamptzDefault now()
expectedDeliveryDate / actualDeliveryDatetimestamptz
draftAt / processingAt / confirmedAt / receivedAt / completedAt / closedAt / cancelledAttimestamptzPer-status timestamps
currencytextDefault VND
exchangeRatedecimal(12,6)Default 1
subtotal / discount / tax / totaldecimal(15,4)Recalculated by updateSummaryFromItems

Indexes: (inventoryLocationId), (merchantId), (merchantId, status), (vendorId).

PurchaseOrderItem fields:

FieldTypeRequiredDescription
purchaseOrderIdtextFK
itemTypetextMATERIAL / PRODUCT_VARIANT (default ProductVariant)
itemIdtextFK target id (polymorphic, no DB FK)
currencytextDefault VND
uomIdtextSoft-ref to UnitOfMeasure.id
multiplierdecimal(15,4)UoM-to-base (default 1)
quantitydecimal(15,4)Ordered qty
receivedQuantitydecimal(15,4)Cumulative received
unitPricedecimal(15,4)Per UoM
discount / taxdecimal(15,4)Per-line
totaldecimal(15,4)Computed
lotNumber / expiryDate / manufactureDatetext / timestamptzPer-line lot/serial metadata
serialNumbersjsonbstring[] — for serialized inventory
landedCostSharedecimal(15,4)Allocated landed cost portion
effectiveCostdecimal(15,4)Generated column (unitPrice + landedCostShare)

Idempotency on add: same (purchaseOrderId, itemType, itemId, uomId) → quantity sums instead of duplicating.


3.8 Vendor / VendorItem

Vendor fields:

FieldTypeRequiredDescription
merchantIdtextOwner
identifiertextAuto, VEN prefix
slugtextURL-safe
namei18n jsonbDisplay
descriptioni18n jsonb
statustextACTIVATED / DEACTIVATED / ARCHIVED (default ACTIVATED)
locationjsonb{ main, sub, long, lat, postCode }
taxNumbertextTax registration
currencytextDefault VND
contactsjsonb (array)Array<IVendorContact> (default [])
notetextFree-form

VendorItem fields — M:N catalog (NO vendorId on Material / ProductVariant):

FieldTypeRequiredDescription
vendorIdtextFK
merchantIdtextDenormalized
itemTypetextMATERIAL / PRODUCT_VARIANT
itemIdtextFK target id
uomIdtextVendor's catalog UoM (soft ref)
unitPricedecimal(15,4)Quoted price
multiplierdecimal(15,4)UoM-to-base conversion
isPreferredbooleanPartial unique per (merchantId, itemType, itemId)
statustextACTIVATED / DEACTIVATED / ARCHIVED
lastInvoicedjsonbSnapshot from latest PO receive: { unitPrice, uomId, multiplier, orderedAt, receivedAt }

Atomic preferred flip: VendorItemRepository.setPreferredAtomic demotes other rows and promotes target in one statement.


3.9 Material / MaterialIdentifier

Material fields:

FieldTypeRequiredDescription
merchantIdtextOwner
identifiertextAuto, MAT prefix
slugtextURL-safe
namei18n jsonbDisplay
descriptioni18n jsonb
statustextMaterialStatuses (default ACTIVATED)
typetextMaterialTypes (default RAW); see source enum for full set
uomjsonbIUomRole{ base, purchase, sale } (default empty strings); see ADR-0005
costdecimal(15,4)Standard cost reference (no default)
weightdecimal(15,4)
categoryIdtextFK to Category
metadatajsonbIMaterialMetadata — by convention contains inventory.allowOversell (default false) + inventory.isInventoryTracked (default true)

MaterialIdentifier fields:

FieldTypeRequiredDescription
materialIdtextFK
schemetextSYSTEM (auto, prefix MAT) / SLUG / SKU / BARCODE / QRCODE
valuetextUnique per (materialId, scheme)

3.10 MaterialRecipe / MaterialRecipeItem

MaterialRecipe fields:

FieldTypeRequiredDescription
merchantIdtextOwner
principalTypetextMATERIAL / PRODUCT_VARIANT
principalIdtextWhat this recipe produces
statustextDRAFT / ACTIVATED / DEACTIVATED
typetextKIT (deduct at sale) / MANUFACTURED (requires ProductionOrder)
versionintBumped on aggregate update

MaterialRecipeItem fields:

FieldTypeRequiredDescription
materialRecipeIdtextFK to MaterialRecipe
principalTypetextPolymorphic component type (MATERIAL or PRODUCT_VARIANT)
principalIdtextFK target id (component)
quantitydecimal(15,4)Required quantity per principal unit
uomIdtextSoft ref
isOptionalbooleanDefault false — when true, missing component does not block production

Indexes: partial unique (principalType, principalId, materialRecipeId) WHERE deletedAt IS NULL; non-unique (materialRecipeId), (uomId).


3.11 ProductionOrder

FieldTypeRequiredDescription
merchantIdtextOwner
productionNumbertextUnique per merchant
targetTypetextPolymorphic produced-thing (MATERIAL / PRODUCT_VARIANT); default ProductVariant
targetIdtextFK target id
materialRecipeIdtextFK to recipe used
plannedQuantity / actualQuantity / scrapQuantitydecimal(15,4)Production tracking
uomtextUoM code (text, not jsonb)
locationIdtextFK to InventoryLocation
statustextProductionOrderStatuses (default DRAFT)
scheduledStartAt / scheduledEndAt / startedAt / completedAt / cancelledAttimestamptzLifecycle timestamps
outputLotNumber / outputExpiryDatetext / timestamptzOutput bucket metadata

3.12 UnitOfMeasure

FieldTypeRequiredDescription
merchantIdtextNULL = system-wide; else merchant override
codetexte.g. kg, box, pair
namei18n jsonbDisplay
categorytextCOUNT / WEIGHT / VOLUME / TIME
referenceCodetextBase unit code (self-ref)
ratiodecimal(15,4)Ratio to base (1.0 for base unit)

Three-level scope: system (merchantId NULL) → merchant override → product/material (via uom jsonb on Material).


4. InventoryTrackingReferenceTypes

ValueUsed by
PURCHASE_ORDERPO receive
SALE_ORDERSale payment success → product deduct + material reserve
KITCHEN_TICKETKitchen ticket consume
KITCHEN_TICKET_ITEMKitchen ticket item-level consume
INVENTORY_TICKETWorkflow tickets (transfer, adjust, count, scrap, return)
PRODUCTION_ORDERProduction consume + output
ADJUSTMENTManual admin entry
UNKNOWNFallback

5. Status Enums

5.1 PurchaseOrderStatuses

ValueCodeStage
DRAFT001_DRAFTItems mutable
PROCESSING203_PROCESSINGItems frozen, awaiting goods
RECEIVED205_RECEIVEDSome/all items received, partial complete
COMPLETED303_COMPLETEDAll items fully received
CLOSED404_CLOSEDTerminal — no further changes
CANCELLED505_CANCELLEDTerminal — voided

5.2 MaterialRecipeStatuses

ValueCode
DRAFT001_DRAFT
ACTIVATED201_ACTIVATED
DEACTIVATED202_DEACTIVATED

5.3 Vendor / VendorItemStatuses

ValueCode
ACTIVATED201_ACTIVATED
DEACTIVATED202_DEACTIVATED
ARCHIVED300_ARCHIVED

5.4 InventoryTicketStatuses

ValueCode
DRAFT001_DRAFT
SUBMITTED200_SUBMITTED
APPROVED250_APPROVED
IN_PROGRESS300_IN_PROGRESS
COMPLETED303_COMPLETED
CANCELLED505_CANCELLED

5.5 ReceivePurchaseOrderItemModes

ValueBehavior
OVERRIDE (default)newReceived = receivedQuantity
ACCUMULATIVEnewReceived = currentReceived + receivedQuantity

6. FixedInventoryTrackingTypes (19)

DirectionTypes
Inbound (6)STOCK_IN, PURCHASE, TRANSFER_IN, RETURN_FROM_CUSTOMER, ADJUSTMENT_IN, PRODUCTION_COMPLETE
Outbound (10)STOCK_OUT, SALE, TRANSFER_OUT, RETURN_TO_VENDOR, ADJUSTMENT_OUT, EXPIRED, LOST, DAMAGED, USED_INTERNAL, USED_AS_MATERIAL
Neutral (2)INVENTORY_COUNT, ADJUSTMENT_NEUTRAL
Custom (1)CUSTOM

7. Cross-entity Invariants

InvariantEnforcement
quantityAvailable = quantityOnHand − quantityReserved (post-condition)Service layer maintains; adjustStock mutates all three
Exactly one default InventoryLocation per merchantInventoryLocationRepository.setDefaultAtomic
Exactly one preferred VendorItem per (merchantId, itemType, itemId)VendorItemRepository.setPreferredAtomic
MaterialRecipeItem.principalId references either Material or ProductVariant (polymorphic via principalType)Schema; service-level zod validates principal exists
InventoryTracking is append-only (no UPDATE except via admin tooling)Repository convention; service writes only on stock change
InventoryItem polymorphism — exactly one (merchantId, itemType, itemId)ensureInventoryItem upsert
Material.metadata.inventory.allowOversell controls forceNonNegative flag passed to adjustStockInventoryService.loadPrincipalRefs
Vendor link to items goes via VendorItem only — no vendorId column on principalsSchema + ADR

8. Soft-delete Behavior

EntitySoft-deleteNotes
InventoryLocation, InventoryItem, InventoryStock, InventoryIdentifier, InventoryTicket, InventoryTicketItem, Vendor, VendorItem, Material, MaterialIdentifier, MaterialRecipe, MaterialRecipeItem, ProductionOrder, PurchaseOrder, PurchaseOrderItem, UnitOfMeasuredeletedAt marker; reads default to IS NULL
InventoryTracking✓ schema-side onlyService treats as immutable audit; never written deletedAt

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