Inventory Tracking
1. Overview
| Property | Value |
|---|---|
| ID | FEAT-INV-TRACK |
| Status | Stable |
| Owner | inventory-team |
| Depends on | InventoryStock (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
| Field | Type | Required | Description |
|---|---|---|---|
inventoryStockId | text | ✓ | FK to the bucket changed |
merchantId | text | ✓ | Denormalized from stock chain |
referenceType | text | ✓ | One of §4; typed<TInventoryTrackingReferenceType> |
referenceId | text | Originating doc id (PO id, sale order id, etc.) — nullable for orphan adjustments | |
uomId | text | ✓ | Unit at write time |
multiplier | decimal(15,4) | ✓ | UoM-to-base factor (default 1) |
quantityBefore | decimal(15,4) | ✓ | Pre-change snapshot |
quantityChange | decimal(15,4) | ✓ | Signed delta |
quantityAfter | decimal(15,4) | ✓ | Post-change snapshot |
effectivePrice | decimal(15,4) | Per-unit cost (PURCHASE writes only) | |
fromLocationId / toLocationId | text | TRANSFER source / destination | |
reasonCode | text | One of §5 (13 reasons) | |
lotNumber / serialNumber / expiryDate | text / timestamptz | Audit-immutable snapshot of moved bucket | |
remainingQuantity | decimal(15,4) | FIFO layer tracker (decrements as outbound consumes) | |
note | text | Free-form (see InventoryTrackingNotes below) | |
createdBy / modifiedBy | text | User audit (anonymous allowed) |
Note: there is no
typecolumn in the schema. The "movement type" (one of 19FixedInventoryTrackingTypes) is derived from(referenceType, reasonCode, sign-of-quantityChange)at read time, not stored separately. Service code typically writesnoteto disambiguate.
Mutability: append-only. Service treats this table as immutable; never writes deletedAt or updates rows in the normal path.
3. Tracking Types
Source:
FixedInventoryTrackingTypesin@nx/core/src/models/schemas/inventory/inventory-tracking/constants.ts. Total 19 + 1CUSTOM.
Inbound (6)
| Type | Used by |
|---|---|
STOCK_IN | Manual receipt without PO |
PURCHASE | PO receive flow |
TRANSFER_IN | Cross-location move (destination side) |
RETURN_FROM_CUSTOMER | Customer return ticket |
ADJUSTMENT_IN | Manual admin increase |
PRODUCTION_COMPLETE | ProductionOrder output recorded |
Outbound (10)
| Type | Used by |
|---|---|
STOCK_OUT | Manual deduction without sale |
SALE | Sale payment success deduction |
TRANSFER_OUT | Cross-location move (source side) |
RETURN_TO_VENDOR | Vendor return ticket |
ADJUSTMENT_OUT | Manual admin decrease |
EXPIRED | Expiry-driven scrap |
LOST | Loss/theft |
DAMAGED | Damage write-off |
USED_INTERNAL | Internal use ticket |
USED_AS_MATERIAL | Kitchen ticket consume (BOM explosion) |
Neutral (2)
| Type | Used by |
|---|---|
INVENTORY_COUNT | Cycle count reconciliation |
ADJUSTMENT_NEUTRAL | Zero-delta admin entry (annotation only) |
Custom (1)
| Type | Used by |
|---|---|
CUSTOM | Open extension — any merchant-defined type via DiscriminationType |
4. ReferenceTypes
Source:
InventoryTrackingReferenceTypes.
| ReferenceType | Use |
|---|---|
PURCHASE_ORDER | PO receive |
SALE_ORDER | Sale payment success → product deduct + material reserve |
KITCHEN_TICKET | Kitchen ticket lifecycle |
KITCHEN_TICKET_ITEM | Per-item BOM consumption |
INVENTORY_TICKET | Workflow tickets (transfer, adjust, count, scrap, return) |
PRODUCTION_ORDER | Production consume + output |
ADJUSTMENT | Manual admin entry without parent doc |
UNKNOWN | Fallback / pre-classification |
5. Reasons
Source:
InventoryTrackingReasons.
| Reason | Use |
|---|---|
DAMAGED | Stock destroyed |
LOST | Misplaced / theft |
EXPIRED | Past expiry date |
CYCLE_COUNT | Audit reconciliation |
CORRECTION | Manual fix |
CUSTOMER_RETURN | RMA from customer |
VENDOR_RETURN | RMA to vendor |
TRANSFER_IN / TRANSFER_OUT | Cross-location |
PRODUCTION_CONSUME | Material used in production |
PRODUCTION_OUTPUT | Goods produced |
RESERVATION | Material reserved for sale |
RESERVATION_RELEASE | Reservation undone (kitchen VOIDED) |
6. Notes
Source:
inventory/src/common/constants.ts:1-18(InventoryTrackingNotes).
| Constant | Value |
|---|---|
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
| Verb | Path | Auth | Permission |
|---|---|---|---|
| 6× CRUD | /inventory-trackings | JWT/BASIC | InventoryTracking.<crud> |
Effectively read-only —
POSTand 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
| Question | Query |
|---|---|
| Movements for a SaleOrder | WHERE reference_type = 'SALE_ORDER' AND reference_id = '<id>' |
| Stock history for a bucket | WHERE 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 type | GROUP BY type, date_trunc('day', created_at) |
10. Side effects on write
| Side effect | When |
|---|---|
merchantId denormalized | From InventoryStock.merchantId at write time |
multiplier snapshot | From line entity's UoM at write time |
effectivePrice snapshot | From PO line unitPrice (only on PURCHASE type) |
11. Related Pages
- Inventory Stock — every change writes a tracking row
- Purchase Order —
PURCHASEtype writer - Material & BOM —
USED_AS_MATERIALwriter - ADR-0004 Worker idempotency via Tracking
- Operations — Audit query patterns