PRD: Inventory Tickets
| Module | Inventory (CORE-06) | PRD ID | PRD-TKT-001 |
| Status | In-progress | Owner | Inventory squad |
| Date | 2026-05-26 | Version | v0.1 |
| Packages | @nx/core · @nx/inventory | URD | TKT |
TL;DR
Gives a merchant one document - the Inventory Ticket - to capture, route, and execute every warehouse operation: transfer between locations, stock adjustment, cycle count, returns, and scrap. Each ticket carries line items and moves through a shared approval lifecycle so a planned movement is only applied to stock once it is approved, with the eventual movement fully traceable to the ticket that authorized it.
1. Context & Problem
Warehouse operations - transferring stock between locations, adjusting quantities, cycle counting, returning goods to a vendor or from a customer, scrapping spoiled or damaged inventory - all need a document that captures intent, moves through approval, and only touches stock at the very end. Inventory already has locations, items, stock, and an immutable tracking trail, but no operation document tying a planned movement to its eventual tracking rows.
Without such a document, every non-purchase stock change is ad-hoc: there is no controlled state between "intended" and "applied", no approval gate, and no single record an owner can point to and say "this is why stock moved". The Inventory Ticket aggregate is that document. It is modelled to cover the full operation space up front - multiple ticket types, one generic lifecycle, polymorphic partner/origin references, and a return/backorder self-link - so later milestones add execution behavior without further schema migration.
2. Goals & Non-Goals
Goals
- A single InventoryTicket header spanning all stock-operation types: Transfer, Adjustment, Cycle Count, Return to Vendor, Return from Customer, Scrap/Disposal, plus Purchase Request, Stock In, and Internal Use.
- A shared lifecycle
DRAFT → SUBMITTED → APPROVED → IN_PROGRESS → COMPLETED / CANCELLED, expressed as typed constants with transition guards (canSubmit/canApprove/canStart/canComplete/canCancel). - Line items (InventoryTicketItem) with planned vs. actual quantities, persisted with merchant-scoped, soft-deleted CRUD alongside the header.
- A schema that reserves the structural columns for downstream execution (polymorphic partner ref, polymorphic origin ref, return/backorder self-link, spawned-PO link, effective-date backdating, reason code) so no later migration is required to add behavior.
Non-Goals
- Stock-effecting lifecycle execution - completing a ticket to create InventoryTracking rows and update stock (URD-TKT-004) is deferred to a later milestone; the CRUD service does not yet implement the transitions.
- Multi-line aggregate write routes (header + items in one call) - deferred with the lifecycle.
- Backorder partial-execution splits - the self-link column is reserved, with no behavior in this increment.
- Quotation / quota-based reservation and negative-stock enforcement at POS - out of module scope per the URD.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Operation coverage | One ticket header models 100% of the planned stock-operation types - no per-type document needed |
| Schema durability | Downstream execution lands with zero additional schema migration (columns reserved up front) |
| Tenancy isolation | Every ticket and line is merchant-scoped; cross-merchant reads return nothing |
| Lifecycle correctness | Every state change passes a transition guard; no illegal transition is persisted |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Inventory staff | Draft a ticket for a warehouse operation, attach its lines, and route it for approval |
| Owner / Manager | Review and approve tickets before they affect stock; cancel mistaken ones |
| System (later milestone) | Apply an approved ticket to stock and write the audit trail on completion |
Core scenarios: create a ticket of a given operation type → add its line items (planned quantities) → submit → approve → start → complete (stock execution deferred to a later milestone) - or cancel from any non-terminal state.
5. User Stories
- As inventory staff, I want to create a ticket of a specific operation type (transfer, adjustment, count, return, scrap), so a planned warehouse movement has one home.
- As inventory staff, I want to attach line items with planned quantities, so the ticket states exactly what should move.
- As inventory staff, I want to submit a ticket for approval, so stock is not touched until someone signs off.
- As an owner, I want to approve or cancel a ticket through guarded transitions, so only valid lifecycle moves are allowed.
- As an owner, I want every ticket and its lines isolated to my merchant, so another merchant can never see or alter them.
- As the platform, I want the schema to already carry partner, origin, return-link, spawned-PO and reason-code columns, so later execution behavior needs no migration.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | InventoryTicket header models the full operation space - 9 types (PURCHASE_REQUEST, STOCK_IN, TRANSFER, INTERNAL_USE, ADJUSTMENT, CYCLE_COUNT, RETURN_TO_VENDOR, RETURN_FROM_CUSTOMER, SCRAP_DISPOSAL), a superset of the 6 the URD enumerates | URD-TKT-001 |
| FR-2 | Shared lifecycle DRAFT → SUBMITTED → APPROVED → IN_PROGRESS → COMPLETED / CANCELLED, as typed statuses with ACTIVE_SET/TERMINAL_SET and canSubmit/canApprove/canStart/canComplete/canCancel guards | URD-TKT-002 |
| FR-3 | Line items (InventoryTicketItem) carry planned vs. actual quantities, persisted with the header | URD-TKT-003 |
| FR-4 | Completing a ticket creates tracking entries and updates stock | URD-TKT-004 |
| FR-5 | Line items reserve lot/serial columns at the schema layer | URD-TKT-005 |
| FR-6 | Per-item location override - header carries source/destination, lines split per bin | URD-TKT-006 |
| FR-7 | Header reserves a polymorphic partner reference (Vendor / Customer) and a polymorphic origin reference for downstream linkage | URD-TKT-001 |
| FR-8 | Header reserves a return/backorder self-link, a spawned-PO link, a reason code, and an effective date for backdating | URD-TKT-006 |
| FR-9 | Header and lines are merchant-scoped with soft-delete; CRUD honors merchant isolation | URD-TKT-002 |
Full requirement text and acceptance criteria live in the Inventory URD. This PRD references them rather than restating them. FR-4 (stock-effecting completion) and the multi-line aggregate write route are scheduled for a later milestone - see §2 Non-Goals and §12.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | When stock execution lands, the stock change and its tracking entry are written together - no stock change without a matching immutable audit entry |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id); gated by inventory permissions |
| Lifecycle safety | State transitions only via guard predicates; terminal states (COMPLETED / CANCELLED) are immutable |
| Schema durability | Structural columns reserved up front so downstream execution needs no migration |
| Soft-delete | Header and lines use soft-delete; soft-deleted records are excluded from standard listings |
| Precision | Planned/actual quantity math uses decimal precision (float(value, 4)) |
| i18n | User-facing type/status/partner labels are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): ticket list, ticket create/edit by operation type, line-item editor, and the lifecycle status control. The ticket controllers and route definitions live in @nx/inventory (controllers/inventory-ticket{,-item}).
9. Data & Domain
| Entity | Role |
|---|---|
InventoryTicket | The operation document - ITI_… identifier, type, status, source/destination location, polymorphic partner, polymorphic origin, return/backorder self-link, spawned-PO link, reason code, effective date, lifecycle timestamps, merchant scope |
InventoryTicketItem | A ticket line - item reference with planned vs. actual quantities; reserves lot/serial and per-bin location columns |
InventoryTicketTypes | The 9 operation types, grouped Procurement / Routine / Returns / End-of-life; PURCHASE_REQUEST is workflow-only (no stock effect, spawns a PO) |
InventoryTicketStatuses | Typed lifecycle statuses + ACTIVE_SET/TERMINAL_SET/isTerminal and the transition guards |
InventoryTicketPartnerTypes | Vendor / Customer with i18n labels for the partner discrimination |
Conceptual only - full schema and invariants in the inventory domain model.
10. Dependencies & Assumptions
Depends on
- Inventory locations (URD-LOC) - tickets reference a source/destination location.
- Stock levels & movement audit (URD-STK · URD-TRK) - the deferred completion step will read stock and write tracking rows.
- Vendors / Customers (URD-VEN) - the polymorphic partner reference points at a vendor or customer.
- Purchase Orders (URD-PO) - a
PURCHASE_REQUESTticket spawns a PO via the reserved spawned-PO link.
Assumptions
- The merchant has at least one inventory location for source/destination references.
- Stock-effecting completion lands in a later milestone before the feature is end-to-end usable.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Lifecycle execution not yet implemented - service is pure CRUD | Tracked as a Non-Goal this increment; transition handlers and aggregate route scheduled for a later milestone |
| Schema breadth (9 types, polymorphic refs) without behavior could drift from real needs | Columns reserved deliberately to avoid migration; behavior validated when execution lands |
| Backorder partial-execution semantics undefined | Self-link column reserved; split behavior is an open design question for the execution milestone |
| Reverting a completed ticket vs. already-written tracking | Open: define the reversal/compensation path once completion writes stock |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (multi-location operations) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | New entities (InventoryTicket, InventoryTicketItem) added with full column set; no later migration planned for execution |
| Launch criteria | This increment: header + line schema and merchant-scoped CRUD verified. Feature-complete (later milestone): submit→approve→start→complete transitions execute and a COMPLETED ticket writes tracking + updates stock |
| Monitoring | Ticket volume per merchant by type, transition-guard rejections, and (post-execution) stock-vs-tracking consistency checks |
13. FAQ
Does completing a ticket move stock today? Not yet - this increment ships the header/line schema and merchant-scoped CRUD. The stock-effecting completion (URD-TKT-004) is scheduled for a later milestone.
Why 9 ticket types when the URD lists 6? The schema models the full operation space up front (adding Purchase Request, Stock In, and Internal Use) so later milestones add execution without a schema migration.
What is PURCHASE_REQUEST for? It is a workflow-only type - it has no direct stock effect and instead spawns a purchase order via the reserved spawned-PO link.
Can one merchant see another's tickets? No - every ticket and line is merchant-scoped (x-merchant-id) with soft-delete; cross-merchant reads return nothing.
How are partial/return deliveries handled? The header carries a return/backorder self-link column, reserved now; the partial-execution split behavior is defined when lifecycle execution lands.
References
- URD: Inventory - Inventory Tickets (
TKT) - requirements - Builds on: Inventory Locations · Stock Levels · Movement Audit Trail · Vendors · Purchase Orders
- Module: Inventory - overview + traceability
- Developer: @nx/inventory · domain model