Skip to content

PRD: Production orders & BOM explosion

ModuleInventory (CORE-06)PRD IDPRD-PRO-001
StatusShippedOwnerInventory squad
Date2026-06-15Versionv1.0
Packages@nx/inventory · @nx/coreURDPRO · REC · TRK

TL;DR

Gives a manufacturing-style merchant an explicit production order - a document that says "produce this much of this finished good from this recipe, at this location". Each order targets a finished good (a sellable variant or a semi-finished material), is bound to an activated recipe (BOM), and carries planned / actual / scrap quantities, a lifecycle (DRAFT → IN_PROGRESS → DONE / CANCELLED), scheduling windows and output-lot traceability for recalls. Behind it sits a multi-level BOM explosion engine that flattens the bound recipe through nested sub-assemblies down to the leaf materials actually drawn from stock - with quantity multipliers propagated along each branch, a maximum nesting depth, and cycle detection - and a dedicated tracking vocabulary (PRODUCTION_CONSUME / PRODUCTION_OUTPUT) that records consumption and output against the order.

1. Context & Problem

The recipe model (REC) lets an F&B merchant attach a versioned bill of materials to a variant, and the runtime path auto-deducts ingredients when a kitchen ticket progresses. That covers sell-then-deduct, but it does not cover make-to-stock: a bakery that bakes 200 loaves at 6am, a central kitchen that batches a sauce, a workshop that assembles a kit. These merchants plan a production run ahead of sale, against a recipe, and need a document to track what was planned, what was actually made, and what was scrapped.

Without a production order there is no home for a planned run: no planned-vs-actual-vs-scrap figures, no production number to reference, no lifecycle to move a run from draft to done, and no output lot to trace a recall back to. There is also no general way to expand a recipe that itself contains sub-assemblies (a dough that is itself a recipe) down to the raw materials a run consumes.

This increment delivers the production-order document, its lifecycle model and identifiers, the multi-level BOM explosion engine that flattens nested recipes to leaf materials, and the production tracking vocabulary that records consumption and output - the foundation a make-to-stock run is built on.

2. Goals & Non-Goals

Goals

  • A production-order document targeting a finished good (variant or material), bound to an activated recipe, scoped per merchant.
  • Carry planned / actual / scrap quantities in the order's unit of measure at decimal precision.
  • A lifecycle status with defined transitions (DRAFT → IN_PROGRESS → DONE / CANCELLED) and a unique, system-generated production number.
  • A named production location, optional scheduling windows, and output lot / expiry for recall traceability.
  • A multi-level BOM explosion engine that flattens a bound recipe through nested sub-assemblies to leaf materials, propagating quantity multipliers, using the latest activated recipe version, and guarding against runaway depth and cycles.
  • A dedicated tracking vocabulary (PRODUCTION_CONSUME / PRODUCTION_OUTPUT, reference type ProductionOrder) so component consumption and finished-good output are recorded as immutable movements against the order.

Non-Goals

  • Automated lifecycle execution - the start/complete actions that physically draw components and post the finished good on completion are a forthcoming increment; this PRD delivers the document, the explosion engine, and the tracking vocabulary they will use.
  • Capacity planning, work-centre scheduling, or labour/overhead costing.
  • Recipe yield and scrap percentage modelling (URD scope) - scrap is captured as an absolute quantity on the order, not derived from a yield rate.
  • Multi-currency production costing or inventory valuation.
  • The recipe-authoring rules themselves (versioning, activation, item uniqueness) - specified in Recipes / BOM (REC).

3. Success Metrics

MetricTarget / signal
Production document coverageA run is represented by one order carrying planned / actual / scrap and a unique production number
Lifecycle integrityStatus only moves along valid transitions; DONE and CANCELLED are terminal
Explosion correctnessA bound recipe flattens to exactly its leaf materials, with per-branch quantities = product of the path
Explosion safetyA cyclic recipe chain or a tree deeper than the maximum is refused, never looped
Audit vocabularyProduction movements are immutable entries typed PRODUCTION_CONSUME / PRODUCTION_OUTPUT and reference the order

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerPlan a production run against a recipe and track planned vs. actual vs. scrap
Production staffWork an order through its lifecycle and record what was actually made
Quality / Recall officerTrace a produced lot back to the order and the components it drew

Core scenario: a bakery owner creates a production order to bake 200 baguettes from the "Baguette - standard" activated recipe at the main kitchen, scheduled for tomorrow 06:00. The system assigns a production number and opens the order in DRAFT. The bound recipe - whose dough is itself a sub-recipe of flour, water and yeast - is flattened to its leaf materials, with each leaf quantity scaled to the planned 200 units. When the run is later worked and completed, component consumption and the finished-good output are recorded as immutable tracking entries against the order.

5. User Stories

  • As an owner, I plan a run by naming the finished good, its recipe, the quantity and the location, so a production order exists to track it.
  • As an owner, I record planned, actual and scrap quantities, so I can see how a run performed.
  • As production staff, I move an order DRAFT → IN_PROGRESS → DONE, or cancel it, so its state reflects reality.
  • As an owner, I want a recipe with sub-assemblies expanded to the raw materials a run consumes, so I see the true draw on stock.
  • As an owner, I want a recipe that references itself in a loop refused, so a run can never compute an endless component list.
  • As a recall officer, I trace a produced lot back to the order and read the components it consumed, so I can scope a recall.

6. Functional Requirements

#RequirementURD ref
FR-1A production order targets a finished good (ProductVariant or Material) and is bound to a material recipeURD-PRO-001..002
FR-2The order carries planned / actual / scrap quantities in its unit of measure at decimal precisionURD-PRO-003
FR-3The order moves through DRAFT → IN_PROGRESS → DONE / CANCELLED; DONE and CANCELLED are terminalURD-PRO-004
FR-4Each order has a unique, system-generated production number, not user-editableURD-PRO-005
FR-5The order names a production location where components are drawn and the good is producedURD-PRO-006
FR-6The order supports scheduling windows and records start / complete / cancel timestampsURD-PRO-007..008
FR-7The order can carry an output lot number and expiry for recall traceabilityURD-PRO-009
FR-8Production orders are merchant-isolated and soft-deletedURD-PRO-010 · URD-CON-002
FR-9A bound recipe is flattened across nested sub-assemblies to leaf materials, using the latest activated recipe version per principalURD-PRO-011..012
FR-10Flattening is bounded by a maximum nesting depth and rejects cyclic recipe chains; each leaf quantity is the product of the quantities along its pathURD-PRO-013..014
FR-11A variant component without an activated sub-recipe is skipped; a material without a sub-recipe is a leaf consumed directlyURD-PRO-015
FR-12Component consumption and finished-good output are recorded as immutable tracking entries referencing the order, typed PRODUCTION_CONSUME / PRODUCTION_OUTPUTURD-PRO-016 · URD-TRK-001..005

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

7. Non-Functional Requirements

AreaRequirement
Tenancy & authzAll operations scoped per merchant (x-merchant-id) and gated by production-order permissions
Identifier integrityThe production number is unique and system-generated; it is never user-supplied
PrecisionPlanned / actual / scrap and component quantities use decimal precision (4 places)
Explosion safetyFlattening is depth-bounded and cycle-guarded - a malformed recipe graph fails fast, never loops
Explosion performanceThe BOM graph is pre-loaded in batched, level-by-level queries so flattening runs in memory without per-node round-trips
ImmutabilityProduction movements are append-only tracking entries; corrections happen via new entries, never edits
i18nUser-facing statuses and reason labels are bilingual ({ en, vi })

8. UX & Flows

Lifecycle

Multi-level BOM explosion

The production surface lets the owner pick the finished good and its recipe, set planned quantity and unit, choose the location, optionally schedule the run and label the output lot, then work the order through its lifecycle. The explosion engine expands the bound recipe to the leaf materials a run consumes.

9. Data & Domain

EntityRole
ProductionOrderThe manufacturing document - target finished good, bound recipe, planned/actual/scrap, status, location, production number, scheduling, output lot
MaterialRecipeThe activated bill of materials the order is bound to; sub-assembly recipes are recursed during explosion
MaterialRecipeItemOne component line - a leaf material consumed directly, or a principal with its own activated sub-recipe
InventoryTrackingThe immutable movement behind production consumption / output, typed by the production reason codes

Conceptual only - full schema and invariants live in the inventory domain model. Relations are soft references; integrity is enforced in the services, not by database constraints.

10. Dependencies & Assumptions

Depends on

  • Recipes / BOM (REC, PRD-REC-001) - a production order is bound to an activated recipe, which the explosion engine flattens.
  • Materials (MAT, PRD-MAT-001) - leaf components of an exploded recipe are materials.
  • Inventory locations (LOC) - an order names the location it produces at.
  • Movement audit trail (TRK) - production consumption / output are recorded as immutable tracking entries.
  • @nx/core - the production-order model, status vocabulary, and production reason codes.

Assumptions

  • A finished good has at least one activated recipe version to bind to.
  • The targeted location exists within the merchant.
  • A production-variant component that should decompose carries its own activated sub-recipe; otherwise it is skipped during explosion.

11. Risks & Open Questions

Risk / questionMitigation / status
A recipe references itself, looping the component listExplosion detects cycles and refuses the graph
A deeply nested recipe tree runs awayExplosion is bounded by a maximum nesting depth and fails fast beyond it
Stale sub-recipe version usedExplosion always resolves the latest activated version per principal
Variant component cannot be decomposedA variant without an activated sub-recipe is skipped (it is not a material), warned, never emitted as a leaf
Lifecycle execution not yet automatedDeliberate - start/complete actions that draw components and post output land in a forthcoming increment; document, engine and tracking vocabulary are in place

12. Release Plan & Launch Criteria

AspectPlan
PhaseP3 (BOM + advanced) - PRO in the URD feature catalog, alongside MAT and REC
RolloutMerchants on manufacturing / F&B verticals; no feature flag
MigrationNone - the production-order table and production reason codes ship with the inventory schema
Launch criteriaAn order can be created with target, recipe, planned quantity and location; a production number is assigned and status opens at DRAFT; status moves only along valid transitions; a bound recipe flattens to its leaf materials with multiplied quantities; cyclic / over-deep graphs are refused; production movements carry the production reason codes and reference the order
MonitoringExplosion failures by reason (cycle, depth), production-order count by status, leaf-material resolution rate

13. FAQ

How is a production order different from a kitchen recipe deduction? A kitchen deduction is sell-then-deduct - it fires when a ticket item progresses. A production order is make-to-stock - a planned run against a recipe, with planned/actual/scrap and its own lifecycle, recorded ahead of (or independent of) any sale.

What does "multi-level BOM" mean here? A recipe component can itself be a principal with its own activated recipe (a dough inside a baguette). Explosion recurses into those sub-assemblies until it reaches leaf materials, multiplying quantities along the way.

What stops a recipe loop from hanging the system? Explosion tracks visited recipes and enforces a maximum nesting depth - a cycle or an over-deep tree is refused immediately rather than looped.

Which recipe version is used? Always the latest activated version for each principal; draft or deactivated versions are ignored.

Does completing an order draw stock automatically today? Not yet - the document, recipe binding, explosion engine and production tracking vocabulary (PRODUCTION_CONSUME / PRODUCTION_OUTPUT) are in place; the automated execution that posts those movements on completion is a forthcoming increment.

References

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