Skip to content

PRD: Lot / serial & expiry identifiers

ModuleInventory (CORE-06)PRD IDPRD-LSE-001
StatusShippedOwnerInventory squad
Date2026-06-15Versionv1.0
Packages@nx/inventory · @nx/core · @nx/searchURDLSE · STK · TRK

TL;DR

Makes stock traceable. The same item at the same location splits into separate stock buckets per lot and per serial, each carrying its own expiry and manufacture date - so a merchant can recall a bad batch, dispose of an expired lot, and follow a serial-tracked unit end to end. A single identifier registry sits alongside: SKU / barcode / QR codes resolve at the product (item) level, IMEI / serial codes at the unit (stock) level, and any (scheme, code) pair is globally unique so one scan always resolves to exactly one inventory entity. Lot and expiry are captured at goods receipt and on tickets, snapshotted immutably on every movement, and expired stock leaves through a dedicated disposal reason.

1. Context & Problem

Stock levels (STK) tell a merchant how much they hold, and the movement trail (TRK) tells them how it changed - but neither, on its own, tells them which physical batch or unit a quantity refers to. A bakery that learns one flour delivery is contaminated, a pharmacy that must pull everything expiring this month, an electronics seller that warranties a single handset by its IMEI - all need stock identity below the item, and a way to turn a scanned code back into the right record.

Without lot/serial bucketing, every unit of an item collapses into one undifferentiated pile: a recall means dumping all of it, an expiry date can't be attached to anything, and a serial number has nowhere to live. Without a unique identifier registry, a barcode is just a string with no guarantee it points at one thing. This increment adds both: traceable stock buckets keyed by lot and serial with expiry/manufacture dates, and a registry that resolves SKU / barcode / QR / IMEI / serial codes to exactly one inventory entity.

2. Goals & Non-Goals

Goals

  • Bucket stock by (item, location, lot, serial) so distinct lots and serials of one item are tracked separately.
  • Carry expiry and manufacture dates on each traceable bucket.
  • Capture lot / serial / expiry at goods receipt (purchase-order lines) and on inventory tickets.
  • Snapshot the lot / serial / expiry that moved into the immutable tracking entry on every movement.
  • An identifier registry that records SKU / barcode / QR at the item level and IMEI / serial at the unit level, with a globally unique (scheme, code) pair so one scan resolves to one entity.
  • Dispose of expired stock through a dedicated movement reason that decrements the right lot.

Non-Goals

  • A dedicated expiring-soon dashboard or automatic FEFO picking engine - the dates are captured and the data model supports it (URD-LSE-013 remains a Could); the operator UI lands with PRD-IOP-001.
  • Inventory valuation by lot (FIFO / LIFO lot costing) - per-bucket average cost is recorded, but lot-level valuation reports are out of scope.
  • Multi-currency stock valuation.
  • Full barcode-scanning hardware integration for stock-takes (URD-CON / Scope).

3. Success Metrics

MetricTarget / signal
Lot separationTwo receipts of the same item with different lots create two distinct stock buckets, not one merged figure
Scan resolutionA scanned (scheme, code) resolves to exactly one inventory entity, system-wide
Identifier integrityNo two active records share the same (scheme, code) pair
Trace completenessEvery movement of a traceable bucket carries the lot / serial / expiry it moved
Recall reachAn owner can identify every location holding a named lot from the stock records alone

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerRecall a bad lot, dispose of expired stock, warranty a unit by its serial
Inventory staffRecord lot / serial / expiry when receiving goods or running a ticket
CashierScan a barcode and land on exactly the right item
Channel integrationLook up an item or unit by a registered identifier

Core scenarios: receive two deliveries of the same ingredient with different lots and expiries → each becomes its own stock bucket → a contamination notice arrives → the owner finds every location holding that lot → the expired lot is disposed through the expiry reason and the trail records the lot that left. In parallel, an electronics seller registers each handset's IMEI against its stock unit; a scan of that IMEI resolves to exactly one unit.

5. User Stories

  • As an owner, I want each lot of an item tracked separately, so I can recall one batch without dumping the rest.
  • As an owner, I want an expiry date on each lot, so I can pull stock before it goes bad.
  • As inventory staff, I want to record lot, serial, and expiry when I receive goods, so traceability starts at the door.
  • As an owner, I want a serial-tracked unit to carry its IMEI / serial, so I can warranty and trace one unit.
  • As a cashier, I want a scanned barcode to resolve to exactly one item, so I never ring up the wrong product.
  • As an owner, I want expired stock removed through a clear "expired" reason, so the audit trail explains the write-off.

6. Functional Requirements

#RequirementURD ref
FR-1Stock is bucketed by (item, location, lot, serial); distinct lots/serials of one item at one location are separate recordsURD-LSE-001 · URD-STK-001
FR-2Each bucket carries lot number, serial number, expiry date, and manufacture dateURD-LSE-002
FR-3Buckets with no lot/serial collapse to one record (NULLs treated as equal), so non-traceable items keep a single bucketURD-LSE-003
FR-4Lot / serial / expiry are captured at goods receipt on PO lines; serial-tracked lines record one serial per received unitURD-LSE-004
FR-5Inventory-ticket lines capture lot / serial / expiry for stock-in and return-from-customer; a serial line must have planned quantity 1URD-LSE-005
FR-6Every movement snapshots the lot / serial / expiry that moved into the immutable tracking entryURD-LSE-006 · URD-TRK-001..002
FR-7An identifier registry records SKU / BARCODE / QRCODE at the item level and IMEI / SERIAL at the unit (stock) levelURD-LSE-007
FR-8Each (scheme, code) pair is globally unique across active records - one code resolves to exactly one inventory entityURD-LSE-008
FR-9Identifier scheme defaults to UNKNOWN when unspecified; only SKU / BARCODE / QRCODE / IMEI / SERIAL are recognizedURD-LSE-009
FR-10Identifiers are merchant-scoped through their principal (item or stock) and use soft-deleteURD-LSE-010
FR-11Expired stock is disposed through a dedicated EXPIRED movement reason that decrements the lotURD-LSE-011
FR-12A production run can stamp its finished-good output with a lot number and expiry dateURD-LSE-012 · URD-PRO-009

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

7. Non-Functional Requirements

AreaRequirement
Identity uniquenessA (scheme, code) pair is unique across all active records system-wide; soft-deleted codes free the pair for reuse
Bucket integrityThe bucket key (item, location, lot, serial) is enforced so equal NULLs still collapse to one record - no accidental duplicate buckets
ImmutabilityThe lot / serial / expiry on a movement entry is an append-only snapshot; corrections happen via new movements
Tenancy & authzIdentifiers resolve their merchant through the principal (item or stock); all operations are merchant-scoped (x-merchant-id) and gated by inventory permissions
SearchabilityRegistered identifiers are denormalized for lookup so a scan resolves quickly without scanning every record
i18nIdentifier scheme labels are bilingual ({ en, vi })

8. UX & Flows

The receipt and ticket surfaces (in apps/client) let staff enter lot, serial, expiry, and manufacture dates per line, and register a unit or item code against the resulting record. A scan of any registered code lands on its single inventory entity.

9. Data & Domain

EntityRole in traceability
InventoryStockA quantity bucket per (item, location, lot, serial); carries expiry and manufacture dates and a per-bucket cost snapshot
InventoryTrackingImmutable movement entry; snapshots the lot / serial / expiry that moved
InventoryIdentifierA registered scan code, polymorphically anchored to an item (SKU/BARCODE/QRCODE) or a stock unit (IMEI/SERIAL); (scheme, code) globally unique

Conceptual only - full schema and invariants live in the inventory domain model. The identifier's principal is a soft reference resolved at runtime, not a database foreign key.

10. Dependencies & Assumptions

Depends on

  • Stock levels (STK, PRD-STK-001) - lot/serial extend the stock bucket key.
  • Movement trail (TRK) - each movement snapshots the lot it moved.
  • Goods receipt & tickets (PO, TKT) - the capture points for lot / serial / expiry.
  • @nx/core - stock, tracking, and identifier models (schemes, principal types, bucket key).
  • @nx/search - denormalizes registered identifiers for fast scan lookup.

Assumptions

  • A merchant chooses which items are lot- or serial-tracked; non-traceable items keep a single NULL-lot bucket.
  • A serial-tracked receipt line represents one physical unit (planned quantity 1).
  • The stock and movement layers (STK, TRK) are already in place as the records lot/serial/expiry attach to.

11. Risks & Open Questions

Risk / questionMitigation / status
Same item, same location, different lots merge into one figureBucket key includes lot + serial, so each lot is its own record
Two records claim the same barcode(scheme, code) is globally unique on active records; a duplicate is refused
Reusing a code after a record is removedUniqueness applies to active records only; soft-deleting a code frees the pair
No expiring-soon screen yetDates are captured and the model supports it; the operator UI ships with PRD-IOP-001; URD-LSE-013 stays a Could
A serial line received with quantity > 1Refused - a serial line must represent exactly one unit

12. Release Plan & Launch Criteria

AspectPlan
PhaseP3 - traceability for F&B (lot/expiry) and retail (serial/IMEI), alongside STK and TRK in the URD feature catalog
RolloutAll merchants; lot/serial capture is opt-in per item, no feature flag
MigrationLot / serial / expiry / manufacture columns added to the stock and tracking records; the identifier registry seeds its recognized schemes
Launch criteriaDistinct lots create distinct buckets; a scanned (scheme, code) resolves to one entity; duplicates are refused; every movement snapshots its lot; expired stock leaves through the EXPIRED reason
MonitoringDuplicate-identifier refusal rate, count of traceable buckets per merchant, expired-disposal movements

13. FAQ

What makes two stock figures "different lots"? The bucket key is (item, location, lot, serial). Receive the same item under a new lot number and it becomes a separate record with its own expiry - the old lot is untouched.

Where do barcodes and serials live? In the identifier registry. SKU / barcode / QR codes attach at the item (product) level; IMEI / serial codes attach at the unit (stock) level. Any (scheme, code) pair is unique system-wide, so one scan resolves to one entity.

Can two products share a barcode? No - a duplicate (scheme, code) is refused for active records. Removing a code (soft-delete) frees the pair for reuse.

Is there an expiring-soon report? The expiry and manufacture dates are captured on every lot, and the data model supports FEFO and expiring-soon visibility (URD-LSE-013, a Could); the operator-facing screen ships with PRD-IOP-001.

How is expired stock removed? Through a dedicated EXPIRED movement reason that decrements the specific lot and records the write-off in the immutable trail - it never silently disappears.

Do non-traceable items get a bucket per anything? No - items without lot/serial keep a single bucket (equal NULLs collapse), so simple retail stock stays simple.

References

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