Skip to content

Inventory Tracking

1. Overview

PropertyValue
IDFEAT-INV-TRACK
StatusStable
Ownerinventory-team
Depends onInventoryStock (every row references one), DiscriminationType (validates type / referenceType / reason)

InventoryTracking is the single audit trail for every stock change in the system. Every adjustStock mutation MUST write a tracking row. The table also doubles as the idempotency ledger for Kafka workers — see ADR-0004.

2. Entity Model

InventoryTracking fields

FieldTypeRequiredDescription
inventoryStockIdtextFK to the bucket changed
merchantIdtextDenormalized from stock chain
referenceTypetextOne of §4; typed<TInventoryTrackingReferenceType>
referenceIdtextOriginating doc id (PO id, sale order id, etc.) — nullable for orphan adjustments
uomIdtextUnit at write time
multiplierdecimal(15,4)UoM-to-base factor (default 1)
quantityBeforedecimal(15,4)Pre-change snapshot
quantityChangedecimal(15,4)Signed delta
quantityAfterdecimal(15,4)Post-change snapshot
effectivePricedecimal(15,4)Per-unit cost (PURCHASE writes only)
fromLocationId / toLocationIdtextTRANSFER source / destination
reasonCodetextOne of §5 (13 reasons)
lotNumber / serialNumber / expiryDatetext / timestamptzAudit-immutable snapshot of moved bucket
remainingQuantitydecimal(15,4)FIFO layer tracker (decrements as outbound consumes)
notetextFree-form (see InventoryTrackingNotes below)
createdBy / modifiedBytextUser audit (anonymous allowed)

Note: there is no type column in the schema. The "movement type" (one of 19 FixedInventoryTrackingTypes) is derived from (referenceType, reasonCode, sign-of-quantityChange) at read time, not stored separately. Service code typically writes note to disambiguate.

Mutability: append-only. Service treats this table as immutable; never writes deletedAt or updates rows in the normal path.

3. Tracking Types

Source: FixedInventoryTrackingTypes in @nx/core/src/models/schemas/inventory/inventory-tracking/constants.ts. Total 19 + 1 CUSTOM.

Inbound (6)

TypeUsed by
STOCK_INManual receipt without PO
PURCHASEPO receive flow
TRANSFER_INCross-location move (destination side)
RETURN_FROM_CUSTOMERCustomer return ticket
ADJUSTMENT_INManual admin increase
PRODUCTION_COMPLETEProductionOrder output recorded

Outbound (10)

TypeUsed by
STOCK_OUTManual deduction without sale
SALESale payment success deduction
TRANSFER_OUTCross-location move (source side)
RETURN_TO_VENDORVendor return ticket
ADJUSTMENT_OUTManual admin decrease
EXPIREDExpiry-driven scrap
LOSTLoss/theft
DAMAGEDDamage write-off
USED_INTERNALInternal use ticket
USED_AS_MATERIALKitchen ticket consume (BOM explosion)

Neutral (2)

TypeUsed by
INVENTORY_COUNTCycle count reconciliation
ADJUSTMENT_NEUTRALZero-delta admin entry (annotation only)

Custom (1)

TypeUsed by
CUSTOMOpen extension — any merchant-defined type via DiscriminationType

4. ReferenceTypes

Source: InventoryTrackingReferenceTypes.

ReferenceTypeUse
PURCHASE_ORDERPO receive
SALE_ORDERSale payment success → product deduct + material reserve
KITCHEN_TICKETKitchen ticket lifecycle
KITCHEN_TICKET_ITEMPer-item BOM consumption
INVENTORY_TICKETWorkflow tickets (transfer, adjust, count, scrap, return)
PRODUCTION_ORDERProduction consume + output
ADJUSTMENTManual admin entry without parent doc
UNKNOWNFallback / pre-classification

5. Reasons

Source: InventoryTrackingReasons.

ReasonUse
DAMAGEDStock destroyed
LOSTMisplaced / theft
EXPIREDPast expiry date
CYCLE_COUNTAudit reconciliation
CORRECTIONManual fix
CUSTOMER_RETURNRMA from customer
VENDOR_RETURNRMA to vendor
TRANSFER_IN / TRANSFER_OUTCross-location
PRODUCTION_CONSUMEMaterial used in production
PRODUCTION_OUTPUTGoods produced
RESERVATIONMaterial reserved for sale
RESERVATION_RELEASEReservation undone (kitchen VOIDED)

6. Notes

Source: inventory/src/common/constants.ts:1-18 (InventoryTrackingNotes).

ConstantValue
ADJUSTMENT_FROM_PRODUCT_VARIANT_CREATE"Adjustment from product variant create form"
ADJUSTMENT_FROM_PRODUCT_VARIANT_UPDATE"Adjustment from product variant update form"
ADJUSTMENT_FROM_PRODUCT_VARIANT_CDC"Adjustment from product variant CDC bridge"
OVERSELL_BLOCKED"OVERSELL_BLOCKED: deduct skipped; insufficient stock with allowOversell=false"
INBOUND_FROM_PURCHASE_ORDER"Inbound from purchase order"
OUTBOUND_FROM_MATERIAL_USED"Outbound from material used"
ADJUSTMENT_FROM_MATERIAL_CREATE"Adjustment from material create from"
ADJUSTMENT_FROM_MATERIAL_UPDATE"Adjustment from material update from"
MATERIAL_RESERVED_FROM_SALE_ORDER"Material reserved at sale-order payment success"

7. Operations

Service surface

InventoryTrackingService (inventory-tracking.service.ts:21-41) is MerchantScopedService with inherited CRUD only — no custom mutators. Reads are filtered by merchant. Writes happen inside other services' stock-mutation flows; no public write API today (intentional — preserves audit integrity).

REST surface

VerbPathAuthPermission
6× CRUD/inventory-trackingsJWT/BASICInventoryTracking.<crud>

Effectively read-only — POST and mutating endpoints exist via CRUD but service layer does not surface a custom write entry point.

8. Idempotency Role

Every Kafka worker uses InventoryTracking as the dedup ledger before any stock mutation:

See ADR-0004 for the full rationale.

9. Query Patterns

QuestionQuery
Movements for a SaleOrderWHERE reference_type = 'SALE_ORDER' AND reference_id = '<id>'
Stock history for a bucketWHERE inventory_stock_id = '<id>' ORDER BY created_at DESC
Oversell incidents (last 24h)WHERE note LIKE 'OVERSELL_BLOCKED%' AND created_at > now() - interval '24h'
Goods received from a vendor (via PO)join PurchaseOrder on referenceId where vendorId = '<id>'
Daily issuance volume by typeGROUP BY type, date_trunc('day', created_at)

10. Side effects on write

Side effectWhen
merchantId denormalizedFrom InventoryStock.merchantId at write time
multiplier snapshotFrom line entity's UoM at write time
effectivePrice snapshotFrom PO line unitPrice (only on PURCHASE type)

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