Skip to content

PRD: Combos & bundles

ModuleProduct (CORE-05)PRD IDPRD-BND-001
StatusShippedOwnerProduct squad
Date2026-06-15Versionv1.0
Packages@nx/core · @nx/commerce · @nx/saleURDBND · VAR

TL;DR

Lets a merchant sell more than one variant as a single thing. A combo is priced once and made of component variants; the moment it is added to the cart the system expands it server-side into one priced lead line plus zero-priced component lines, so the kitchen sees every component and the bill shows one price. An add-on attaches a free related variant to a host line at the point of sale; a frequently-bought-together entry suggests a related variant and can carry its own price override. The whole bundle graph is created atomically with the variant, combos may nest, and expansion is guarded against runaway depth, cycles, and empty combos.

1. Context & Problem

The catalogue can already describe a single sellable variant (VAR) and price it through a fare set (FAR). It cannot describe a relationship between two variants - "this meal is a burger plus fries plus a drink", "offer a free sauce with this", "people who buy this also buy that". Merchants sell these every day, and a POS that can only ring up flat variants forces staff to key each component by hand and guess the price.

Worse, a combo is not just a label: the kitchen needs to see every physical component to make it, inventory needs to deduct each component's stock, and the bill needs to show one price - not the sum of the parts. Without a first-class bundle, a "combo" is either an un-decomposable single line (kitchen and stock are blind to the components) or a pile of separately-priced lines staff must discount to zero by hand.

This increment makes the bundle relationship first-class. The owner declares combo / add-on / FBT relationships on a variant, and at the point of sale the system expands a combo into a priced lead and its zero-priced components automatically, attaches add-ons and FBT items to a lead line on demand, and protects the expansion from misconfigured graphs.

2. Goals & Non-Goals

Goals

  • Relate two variants with a typed bundle: COMBO, ADDON, or FBT (a lead variant → a related variant).
  • A combo carries its own price; its components are expanded into zero-priced child lines at cart-add.
  • Multiply each component by the combo quantity, merge a component reached on multiple branches into one line.
  • Support nested combos with a hard depth ceiling, and refuse depth-exceeded, cyclic, or empty combos with a specific reason.
  • Attach an add-on (always free) or an FBT item (its own / overridden price) to an existing top-level lead line at the point of sale.
  • Create the whole bundle graph atomically with the variant aggregate, and record which combo and bundle rows produced each line for downstream use.

Non-Goals

  • Pricing the combo itself - the combo lead is priced through its fare set (FAR); this PRD only zeroes the components.
  • Stock figures and reservation mechanics - bundle expansion triggers reservation, but the stock model lives in Inventory.
  • Bill-of-materials / manufacturing recipes (a manufactured variant's inputs) - a separate concern in Inventory.
  • Promotion-driven discounts on a bundle - discount calculation (CMP) is a separate, in-progress feature.
  • Cartesian generation of combos from option axes - combos are declared, not generated (OPT).

3. Success Metrics

MetricTarget / signal
Combo decompositionEvery combo on an order produces one priced lead plus its full set of zero-priced components
Price integrityA combo's bill amount equals its own fare, never the sum of its components
Expansion safetyNo order is created from a combo that exceeds the depth limit, forms a cycle, or has no components
Quantity accuracyA component's line quantity equals its per-combo quantity × the combo quantity, summed across branches
Attach correctnessAn add-on / FBT item attaches only to a matching top-level lead line; a combo is never attached this way

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerDeclare a combo's components, attachable add-ons, and FBT suggestions on the catalogue
CashierRing up a combo as one line, attach an add-on or accept a suggestion at the point of sale
Kitchen / FulfilmentSee every physical component of a combo to prepare it

Core scenario: an owner builds a "Lunch Combo" variant priced at one amount and declares its components - 1 burger, 1 fries, 1 drink. A cashier adds the combo to the cart; the system creates a priced lead line for the combo and three zero-priced component lines beneath it. The cashier attaches a paid extra sauce (FBT) to the combo lead and a free napkin set (add-on) to a burger line. Ordering two combos scales every component line to twice its quantity automatically.

5. User Stories

  • As an owner, I declare a combo's components with their quantities, so the kitchen and stock see the real parts while the guest sees one price.
  • As an owner, I offer a free add-on on a variant, so staff can attach it at the point of sale without re-pricing.
  • As an owner, I suggest a frequently-bought-together item with its own price, so up-selling is one tap.
  • As a cashier, I add a combo as a single line and let the system fill in the components, so I never key parts by hand.
  • As a cashier, I bump a combo's quantity and watch every component scale, so the order stays correct.
  • As kitchen staff, I see each component of a combo, so I can make exactly what was sold.

6. Functional Requirements

#RequirementURD ref
FR-1A bundle relates a lead variant to a related variant with one of three types: COMBO, ADDON, FBTURD-BND-001
FR-2A combo is composed of component variants each with its own quantity; the combo is priced, components are zeroedURD-BND-002
FR-3An add-on relates an always-free related variant to a host variantURD-BND-003
FR-4An FBT entry suggests a related variant, is directional, and may carry its own price override (fixed / percentage / per-unit / tiered) or fall back to the suggested variant's fareURD-BND-004
FR-5The same (type, lead, related) relationship cannot be duplicatedURD-BND-005
FR-6Bundles of a lead and type carry a display orderURD-BND-006
FR-7Bundles are created atomically with the variant aggregate; any failure rolls the whole create backURD-BND-007 · URD-VAR-003
FR-8Adding a combo to the cart expands it server-side into a priced lead line plus zero-priced component child lines linked to the leadURD-BND-008
FR-9Each component's quantity is its per-combo quantity × the combo quantity; a component reached on multiple branches is merged into one line with summed quantityURD-BND-009
FR-10Combos may nest; expansion recurses up to a maximum depth of five levelsURD-BND-010
FR-11A combo exceeding the depth limit, forming a cycle, or having no components is refused with a specific reasonURD-BND-011
FR-12Combo component lines cannot be edited directly; the owner edits the combo lead and children scale automaticallyURD-BND-012
FR-13A combo product variant must declare at least one combo component at creationURD-BND-013
FR-14An add-on or FBT item attaches at the point of sale to an existing top-level lead line, validated against the configured relationship; a combo cannot be attached this wayURD-BND-014
FR-15An add-on still reserves inventory for its component even though it is freeURD-BND-015
FR-16Each combo lead and component line records which combo and bundle rows produced it for downstream kitchen / refund / auditURD-BND-016

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

7. Non-Functional Requirements

AreaRequirement
Server-authoritativeCombo expansion and the bundle's type / price are decided on the server from the stored relationship, never trusted from the client
AtomicityA combo's lead and every component line are written together; a stock-guard failure rolls back before any row is mutated
Bounded expansionA graph pre-load and an iterative flatten cap work at a fixed maximum depth, with explicit cycle and empty-combo guards - a misconfigured graph can never loop forever
PrecisionComponent quantities are multiplied and summed at decimal precision (4 places)
Tenancy & authzAll operations scoped per merchant (x-merchant-id) and gated by product / sale permissions
AuditabilityEvery combo and component line carries the combo lead and the bundle rows that produced it
i18nCombo and component names are carried bilingual ({ en, vi }) in each line's snapshot

8. UX & Flows

The catalogue surface lets the owner declare, on a variant, its combo components (with quantities), its attachable add-ons, and its FBT suggestions (with an optional price override). At the point of sale a combo appears as a single priced line with its components nested beneath it; staff can attach an add-on or accept an FBT suggestion against a lead line.

9. Data & Domain

EntityRole in bundling
ProductBundlerOne typed relationship (COMBO / ADDON / FBT) from a lead variant to a related variant, carrying quantity, an optional price-override basis, and a display order
ProductVariantA combo-typed variant is the lead of its component bundle rows; a plain variant is a physical component leaf
SaleOrderItemAt cart-add a combo becomes a priced lead item plus zero-priced child items linked back to the lead

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

10. Dependencies & Assumptions

Depends on

  • Variants & aggregate create (VAR, URD-VAR-003) - bundle rows are created together with the variant in one transaction.
  • Variant types (URD-VAR-006) - a variant's COMBO type is what makes the expander recurse into its bundle.
  • Fares & pricing (FAR) - the combo lead is priced through its own fare set; components are zeroed.
  • Inventory (Inventory) - each expanded component reserves stock at cart-add.
  • @nx/sale - the order service that expands a combo and attaches add-ons / FBT at the point of sale.

Assumptions

  • A combo variant always declares at least one component; a plain (non-combo) variant referenced by a combo row is a physical leaf.
  • The owner keeps the bundle graph acyclic and shallow; the depth, cycle, and empty-combo guards are safety nets, not the design intent.

11. Risks & Open Questions

Risk / questionMitigation / status
A misconfigured combo references itself or nests too deepA bounded pre-load + iterative flatten enforce a fixed maximum depth and detect cycles; the add is refused with a specific reason
A combo with no components reaches the cartEmpty combos are refused at expansion; a combo variant must also declare a component at creation
Client tampering with combo price or componentsExpansion and the bundle type / price are server-authoritative, derived from the stored relationship
Editing a combo component line directly desynchronizes the comboComponent lines are not directly editable; the owner edits the lead and components scale automatically
A free add-on silently oversells stockAn add-on reserves inventory for its component even though its price is zero

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - BND in the URD feature catalog, alongside VAR
RolloutAll merchants; no feature flag
MigrationNone - bundle rows are created on the existing variant aggregate-create path
Launch criteriaA combo adds as a priced lead plus zero-priced components; component quantities scale with the combo; depth-exceeded, cyclic, and empty combos are refused; add-ons / FBT attach only to a matching top-level lead; a combo cannot be attached as an add-on
MonitoringCombo-expansion refusal rate by reason (depth, cycle, empty), attach-mismatch rate, combo-vs-component price consistency

13. FAQ

Why are a combo's components priced at zero? Because the combo itself carries the price. The lead line holds the combo's fare; the components are there so the kitchen can make them and inventory can deduct them, not to be billed.

What happens when I add two of the same combo? Every component line scales: a component's quantity is its per-combo amount times the combo quantity. The same component reached through more than one branch is merged into a single line with the quantities summed.

Can a combo contain another combo? Yes - combos may nest. Expansion recurses up to a fixed depth of five levels; beyond that, or if the chain loops back on itself, the add is refused with a specific reason.

How do add-ons differ from FBT? An add-on is always free and is attached to a host line. A frequently-bought-together item is a directional suggestion that carries its own price - or an override (fixed, percentage, per-unit, or tiered) - and is added when its trigger variant is bought.

Can I attach a combo to another line as an add-on? No - combos are server-expanded as their own lead line and cannot be attached. Only add-on and FBT relationships attach to an existing top-level lead line.

Can I edit a combo's component line on the order? No - components are not directly editable. Edit the combo lead instead and the components scale automatically.

References

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