Skip to content

PRD: Recipes / Bill of Material

ModuleInventory (CORE-06)PRD IDPRD-REC-001
StatusIn-progressOwnerInventory squad
Date2026-04-15Versionv0.1
Packages@nx/inventory · @nx/sale · @nx/coreURDREC · MAT

TL;DR

Lets an F&B merchant declare which raw materials a product consumes as a versioned recipe (bill of materials), then have those materials auto-deducted the moment a dish is prepared. When a kitchen-ticket item progresses - or a payment succeeds - the active recipe explodes, each material is decremented at the merchant's default location, and an immutable "Used as Material" movement is recorded. The result: ingredient stock stays accurate without anyone touching it by hand.

1. Context & Problem

F&B merchants sell prepared products - dishes, drinks - whose stock impact is not on the finished product but on its underlying raw materials. Inventory already tracks variants and receives goods via purchase orders, but there is no way to declare what materials a product consumes, nor to draw those materials down when an item is prepared. Without recipes, ingredient stock can only be corrected by manual adjustment, which is error-prone and breaks cost/usage tracking for the F&B vertical KICKO targets.

This increment introduces the Material Recipe (bill of materials): a versioned recipe attached to a product variant, a set of recipe items (material + quantity + unit), and a runtime path that explodes the active recipe and decrements its materials - wired from the kitchen-ticket lifecycle and from the payment-success path. It builds on the existing material catalog and the stock/tracking primitives.

2. Goals & Non-Goals

Goals

  • A versioned recipe attached to a product variant (default version 1.0), unique per (principal, version).
  • Recipe items declaring a material, a quantity (decimal, 4 places), and a unit; the same material appears at most once per recipe; soft-delete allows re-adding.
  • Activate / deactivate so only the activated recipe version is used at runtime.
  • Runtime explosion: a kitchen-ticket-item status change (and the payment-success path) triggers recipe lookup, decrements each component at the default location, writes a "Used as Material" tracking entry, and emits a material.stock-changed event.
  • Materials as a first-class inventory item type - material inventory creation, with SALE_ORDER added to InventoryTrackingReferenceTypes.

Non-Goals

  • Multi-level BOM / sub-assemblies - single-level only.
  • Recipe yield and scrap percentage.
  • Honoring a non-default kitchen location (URD-REC-208) - runtime uses the merchant default.
  • Explode-at-sale BOM for non-F&B retail.

3. Success Metrics

MetricTarget / signal
Auto-deduction coverage100% of prepared-item completions deduct materials via the active recipe (no manual adjustment)
Stock accuracyMaterial stock-on-hand after explosion = sum of recipe-item quantities × items prepared
IdempotencySame (referenceType, referenceId) replayed produces exactly one tracking entry
Versioning integrityOnly the activated recipe version is ever exploded; prior versions preserved

4. Personas & Use Cases

PersonaGoal in this feature
OwnerDefine recipes per dish, control which version is live, see ingredient usage
Kitchen / Inventory staffHave materials drawn down automatically as items are prepared
SystemExplode the active recipe on kitchen/payment events and write the audit trail

Core scenarios: owner creates a recipe for a variant → adds recipe items (material + qty + unit) → activates it → a kitchen-ticket item progresses (or payment succeeds) → the active recipe explodes, materials decrement at the default location, a "Used as Material" entry is logged, and a material.stock-changed event fires.

5. User Stories

  • As an owner, I want to attach a recipe to a product variant, so the dish knows which materials it consumes.
  • As an owner, I want to add recipe items with a material, quantity, and unit, so each ingredient draw-down is precise.
  • As an owner, I want to activate exactly one recipe version, so runtime always uses the correct definition.
  • As an owner, I want editing an activated recipe to create a new version, so historical recipes are preserved.
  • As kitchen staff, I want materials to deduct automatically when an item is prepared, so I never adjust ingredient stock by hand.
  • As the system, I want explosion to be idempotent and to no-op safely when no recipe or default location exists, so replays and edge cases don't corrupt stock.

6. Functional Requirements

#RequirementURD ref
FR-1Create a versioned recipe attached to a product variant (default 1.0); (principal, version) uniqueURD-REC-001..003
FR-2Recipe items declare material + quantity (4dp) + unit; same material at most once; soft-delete allows re-addingURD-REC-004..006 · URD-REC-101..105
FR-3Activate / deactivate a recipe; only the activated version is used at runtimeURD-REC-007..008 · URD-REC-011
FR-4Editing an activated recipe creates a new version; the original is preserved; versions are listable per principalURD-REC-009..010
FR-5A kitchen-ticket-item status change triggers recipe lookup and decrement prep at the default locationURD-REC-201
FR-6Each component is decremented; a "Used as Material" tracking entry is created; stock updates at the default locationURD-REC-202..203
FR-7The activated recipe for the variant is the one exploded; a material.stock-changed event is emitted after explosionURD-REC-204..205
FR-8No default location → warning logged, skipped (no hard failure); no activated recipe → silent no-opURD-REC-206..207
FR-9Materials are a first-class inventory item type with material inventory creation; barcode/identifier schemes supportedURD-MAT-001..007 · URD-MAT-101..106

Full requirement text and acceptance criteria live in the Inventory URD. This PRD references them rather than restating them. URD-REC-208 (non-default kitchen location) is explicitly out of scope for this increment.

7. Non-Functional Requirements

AreaRequirement
Data integrityEach material decrement and its "Used as Material" tracking entry are written together - no stock change without a matching immutable audit entry
IdempotencyExplosion is idempotent per (referenceType, referenceId); replays produce no duplicate movements
Tenancy & authzAll operations scoped per merchant (x-merchant-id); recipe mutation gated by inventory permissions
PrecisionRecipe-item quantities and stock math use float(value, 4)
ResilienceMissing default location or missing active recipe degrade gracefully (warn / no-op), never a hard failure on the kitchen/payment path
EventingRuntime explosion is routed through a dedicated material worker and emits material.stock-changed
i18nMaterial names and user-facing labels are bilingual ({ en, vi })

8. UX & Flows

Key screens (in apps/client): the recipe editor on a product variant (add/edit recipe items, activate/deactivate version) and the materials catalog. Runtime explosion has no UI - it is event-driven.

9. Data & Domain

EntityRole
MaterialA raw ingredient tracked separately from sellable products; has identifiers (SKU/BARCODE/QR) and an i18n name
MaterialRecipeA versioned recipe attached to a product variant; one activated version drives runtime
MaterialRecipeItemA recipe line - material ref, quantity (4dp), unit
Material inventory itemA first-class inventory item for the material, so stock and tracking apply
Movement recordImmutable "Used as Material" tracking entry written on decrement

Conceptual only - full schema and invariants in the inventory domain model.

10. Dependencies & Assumptions

Depends on

  • Materials catalog (URD-MAT) - recipe items reference existing materials.
  • Stock levels & movement audit (URD-STK · URD-TRK) - decrement builds on stock and tracking primitives.
  • Inventory locations (URD-LOC) - components decrement at the merchant default location.
  • Sale / kitchen events (@nx/sale) - kitchen-ticket-item status changes and the payment-success path trigger explosion.

Assumptions

  • The merchant has a default inventory location.
  • Materials exist and are linked to inventory items before a recipe references them.
  • The product variant a recipe attaches to is already in the catalog.

11. Risks & Open Questions

Risk / questionMitigation / status
Decrement and tracking entry could diverge on partial failureWritten together; no stock change without a matching movement record
Kitchen item with no activated recipeSilent no-op - documented, not an error
Merchant without a default locationWarning logged, explosion skipped; no hard failure
Non-default kitchen location not honoredOut of scope (URD-REC-208); runtime uses merchant default - keeps REC In-progress
Multi-level BOM / sub-assemblies needed laterOut of scope; single-level only this increment

12. Release Plan & Launch Criteria

AspectPlan
PhaseP3 (BOM + Advanced) - see URD feature catalog
RolloutF&B merchants; no feature flag
MigrationNew entities (material recipe + recipe item); no backfill
Launch criteriaCreate→activate→kitchen-event→material decrement + "Used as Material" entry verified end-to-end; idempotent replay verified; no-op/skip paths verified
Monitoringmaterial.stock-changed event volume, explosion error/skip rate, material stock-vs-movement consistency checks

13. FAQ

Why deduct materials instead of the finished product? F&B stock impact lands on ingredients, not the dish. The recipe maps a variant to the materials it consumes so the right stock moves.

What triggers the deduction? A kitchen-ticket-item status change, and also the payment-success path. Both route through a dedicated material worker.

Which recipe version is used? Only the activated version. Editing an activated recipe creates a new version and preserves the original.

What happens if there's no recipe or no default location? No activated recipe is a silent no-op; no default location logs a warning and skips - neither blocks the kitchen or payment flow.

Does this support multi-level BOM? No - single-level only this increment. Sub-assemblies and recipe yield/scrap are out of scope.

References

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