URD: Product
| Module | CORE-05 | Version | v0.7 |
|---|---|---|---|
| Status | Built | Date | 2026-06-04 |
Business documentation. This URD is Product's feature list - each feature below is one Functional Area (
<AREA>). The same<AREA>keys the feature's PRDs (PRD-<AREA>-NNN) and tests (TC-<AREA>-NNN), and each feature is listed in the Delivery feature catalog. See the Feature Spine convention.
1. Purpose
Define the user-facing requirements for product catalogue management: how owners create and maintain products, the variants they actually sell, the prices (fares) those variants carry, the categories that organize them, and the identifiers used to look them up. The catalogue is the foundation every selling channel reads from.
2. Scope
| Included | Excluded |
|---|---|
| Product create / update / status management | Bill of Materials & recipes → Inventory |
| Categories (with add-on flag) | Stock levels per location → Inventory |
| Variants and aggregate variant operations | Order processing & checkout → Orders |
| Fares and fare sets (pricing input) | Promotion discount calculation (In-progress) |
| Multi-scheme identifiers (SYSTEM, SLUG, SKU, BARCODE, QRCODE) | Unit-conversion engine (Planned) |
| Sale-channel availability | CSV / bulk import (Planned) |
| Variant types & units of measure | Technical API specifications → developer docs |
| Role-based data filtering |
Conceptual definitions only - concrete schema and behavior live in commerce domain model and pricing fares.
3. Definitions
| Term | Definition |
|---|---|
| Product | A base item in a merchant's catalogue. |
| Variant | A sellable unit of a product (e.g. a size or flavour). What is actually sold and priced. |
| Variant type | Classification driving stocking & bundling: STORABLE, CONSUMABLE, SERVICE, KIT, COMBO, MANUFACTURED. |
| Option | A way a product varies (Size, Ice level…), owned by one product; each variant chooses one of its values. |
| Option value | A choice on an option (S / M / L); unique within its option. |
| Combination | The set of values a variant takes - its identity within the product. |
| Fare set | The pricing container linked 1-to-1 to a variant. |
| Fare | A price entry inside a fare set; can be a base price, an override, or a quantity/time/channel tier. |
| Category | A product grouping within a merchant; can be flagged add-on. |
| Identifier | A scheme-based code (SYSTEM, SLUG, SKU, BARCODE, QRCODE) used for lookup. |
| Bundle | A combo, add-on, or frequently-bought-together relationship between variants. |
| Sale-channel product | A mapping controlling which products appear in which channels. |
4. Conceptual Model
Conceptual only - full schema lives in the commerce domain model and pricing fares.
5. Feature Catalog
The feature list of this module. Each row is one feature (a Functional Area). Detail in §6. Mirrored in the Delivery feature catalog.
| Feature ID | Feature | Phase | Status | Priority |
|---|---|---|---|---|
PRD | Product Catalogue | P1 | Built | High |
CAT | Categories | P1 | Built | High |
VAR | Variants | P2 | Built | High |
BND | Combos & Bundles | P2 | Built | High |
OPT | Product Options | P2 | Built | High |
FAR | Fares / Pricing | P2 | Built | High |
PID | Identifiers | P1 | Built | High |
CMP | Promotions | P3 | In-progress | Medium |
ACC | Access Control | P1 | Built | High |
SCH | Sale Channels | P2 | Built | High |
Status: live from Plane where mapped, otherwise registry-declared. Vocabulary mirrors Plane (state-group / phase).
6. Features
One sub-section per feature, in catalog order. Each feature keeps its description, requirements, and acceptance together. Priority = MoSCoW (Must / Should / Could / Won't).
PRD - Product Catalogue Built
Feature ID: products/PRD · Phase: P1 · PRDs: - · Dev: @nx/commerce
What it does for users: owners create products within a merchant - each with multilingual name/description, a system identifier and slug, a default variant, images, sale-channel assignment, and a full lifecycle (deactivate / reactivate / archive). Products are searchable and listable, role-filtered.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-PRD-001 | M | Owner can create a product within a merchant | Built |
| URD-PRD-002 | M | Each product gets a system-generated identifier and slug | Built |
| URD-PRD-003 | M | Product name & description support multiple languages | Built |
| URD-PRD-004 | M | A default variant is created with each new product | Built |
| URD-PRD-005 | M | Owner can update product information | Built |
| URD-PRD-006 | M | Owner can deactivate / reactivate / archive a product | Built |
| URD-PRD-007 | M | Product slug is unique within the same merchant | Built |
| URD-PRD-008 | M | User can find a product by ID or slug | Built |
| URD-PRD-009 | M | User can view a product list (role-filtered) | Built |
| URD-PRD-013 | S | Owner can attach images / media to products | Built |
| URD-PRD-014 | S | Owner can assign products to specific sale channels | Built |
| URD-PRD-015 | S | Archived products are read-only (terminal status) | Built |
| URD-PRD-016 | S | Products support a parent-child hierarchy | Built |
| URD-PRD-017 | S | User can search products by name (partial, i18n-aware) | Built |
| URD-PRD-018 | S | User can filter the product list by category | Built |
| URD-PRD-019 | C | Owner can import a catalogue from CSV / spreadsheet | Planned |
Acceptance
AC-PRD-01: Product creation
| Given | When | Then |
|---|---|---|
| Owner with an active merchant | Creates a product with name + category | Product created with generated identifiers; default variant created; product linked to merchant |
| Slug would collide | System ensures a unique slug within the merchant |
CAT - Categories Built
Feature ID: products/CAT · Phase: P1 · PRDs: - · Dev: @nx/commerce
What it does for users: owners group products into categories with multilingual names; categories can be flagged as add-on to drive add-on selling at the point of sale.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-CAT-001 | M | Owner can create categories within a merchant | Built |
| URD-CAT-002 | M | Category name supports multiple languages | Built |
| URD-CAT-003 | M | Owner can update and delete categories | Built |
| URD-CAT-004 | S | A category can be flagged as add-on | Built |
Acceptance
AC-CAT-01: Category management
| Given | When | Then |
|---|---|---|
| Owner with an active merchant | Creates a category with a multilingual name | Category created and scoped to the merchant |
| Category flagged as add-on | Saved | Add-on flag is persisted on the category |
VAR - Variants Built
Feature ID: products/VAR · Phase: P2 · PRDs: - · Dev: @nx/commerce
What it does for users: owners add multiple sellable variants per product, each with a type (STORABLE/CONSUMABLE/SERVICE/KIT/COMBO/MANUFACTURED), units of measure, identifiers, an effective date range, and bundle relationships - created or updated atomically together with their info, fare set, fares, and identifiers.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-VAR-001 | M | Every product has at least one variant; status can change | Built |
| URD-VAR-002 | M | Owner can create additional variants | Built |
| URD-VAR-003 | M | Aggregate create: variant + info + fare set + fares + identifiers in one atomic step | Built |
| URD-VAR-004 | M | Aggregate update: update a variant with all related data atomically | Built |
| URD-VAR-005 | M | Each variant has a system-generated identifier | Built |
| URD-VAR-006 | M | Variant type (STORABLE / CONSUMABLE / SERVICE / KIT / COMBO / MANUFACTURED) is selectable | Built |
| URD-VAR-007 | S | Variant can have an effective date range | Built |
| URD-VAR-008 | S | Owner can assign SKU / BARCODE / QRCODE identifiers | Built |
| URD-VAR-009 | S | Variant stores base / purchase / sale units of measure | Built |
| URD-VAR-010 | S | Bundles relate variants as combo / add-on / frequently-bought-together | Built |
| URD-VAR-011 | C | A variant's resolved price can be read via a dedicated endpoint | Planned |
| URD-VAR-012 | C | Unit conversions between purchase and sale units | Planned |
Acceptance
AC-VAR-01: Aggregate variant
| Given | When | Then |
|---|---|---|
| An existing product | Owner creates a variant with info, fares, and identifiers in one step | Variant + info + fare set + fares + identifiers created atomically |
| Any sub-step fails | Entire operation rolls back; nothing is persisted |
BND - Combos & Bundles Built
Feature ID: products/BND · Phase: P2 · PRDs: PRD-BND-001 · Dev: @nx/commerce · @nx/sale
What it does for users: owners relate two variants with a typed bundle - a combo (priced once, made of component variants), an add-on (a free related variant attached to a host line), or a frequently-bought-together suggestion (directional, with its own optional price override). At the point of sale a combo is expanded server-side into one priced lead line plus zero-priced component lines, component quantities scale with the combo quantity, and add-ons / FBT items attach to an existing top-level lead line. The expansion is guarded against runaway depth, cycles, and empty combos, and the whole bundle graph is created atomically with the variant. (This is the detailed feature behind the URD-VAR-010 bundle mention.)
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-BND-001 | M | A bundle relates a lead variant to a related variant with one of three types: COMBO, ADDON, FBT | Built |
| URD-BND-002 | M | A combo is composed of component variants each with its own quantity; the combo is priced, its components are zeroed | Built |
| URD-BND-003 | M | An add-on relates an always-free related variant to a host variant | Built |
| URD-BND-004 | S | An 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 fare | Built |
| URD-BND-005 | M | The same (type, lead, related) relationship cannot be duplicated | Built |
| URD-BND-006 | S | Bundles of a lead and type carry a display order | Built |
| URD-BND-007 | M | Bundles are created atomically with the variant aggregate; any failure rolls the whole create back | Built |
| URD-BND-008 | M | Adding a combo to the cart expands it server-side into a priced lead line plus zero-priced component child lines linked to the lead | Built |
| URD-BND-009 | M | Each component's quantity is its per-combo quantity × the combo quantity; a component reached on multiple branches is merged into one line with summed quantity | Built |
| URD-BND-010 | S | Combos may nest; expansion recurses up to a maximum depth of five levels | Built |
| URD-BND-011 | M | A combo exceeding the depth limit, forming a cycle, or having no components is refused with a specific reason | Built |
| URD-BND-012 | M | Combo component lines cannot be edited directly; the owner edits the combo lead and children scale automatically | Built |
| URD-BND-013 | M | A combo product variant must declare at least one combo component at creation | Built |
| URD-BND-014 | S | An 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 way | Built |
| URD-BND-015 | S | An add-on still reserves inventory for its component even though it is free | Built |
| URD-BND-016 | S | Each combo lead and component line records which combo and bundle rows produced it for downstream kitchen / refund / audit | Built |
Acceptance
AC-BND-01: Combo expands at cart-add
| Given | When | Then |
|---|---|---|
| A combo priced once, made of 1 burger + 1 fries + 1 drink | Cashier adds the combo to the cart | A priced lead line plus three zero-priced component lines linked to the lead are created |
| The same combo, quantity two | Cashier adds it | Each component line is scaled to twice its per-combo quantity |
AC-BND-02: Combo expansion guards
| Given | When | Then |
|---|---|---|
| A combo nested beyond five levels | Cashier adds it | Refused - the combo nesting is too deep |
| A combo that transitively references itself | Cashier adds it | Refused - a combo cycle is detected |
| A combo variant with no components | Cashier adds it | Refused - the combo has no components |
AC-BND-03: Attach an add-on / FBT
| Given | When | Then |
|---|---|---|
| A top-level lead line and a configured add-on relationship | Cashier attaches the add-on | A free add-on line is created under the lead and its stock is reserved |
| A configured FBT relationship | Cashier attaches the FBT item | A line carrying its own (possibly overridden) price is created under the lead |
| A combo relationship | Cashier tries to attach it | Refused - a combo is server-expanded, not attachable |
| The item does not match the bundle's configured lead / related variant | Cashier attaches it | Refused - the bundle relationship does not match |
AC-BND-04: Combo lines stay consistent
| Given | When | Then |
|---|---|---|
| A combo on the order | Owner tries to edit a component line directly | Refused - components are not directly editable |
| The same combo | Owner edits the combo lead's quantity | Every component line scales automatically |
AC-BND-05: Bundle creation
| Given | When | Then |
|---|---|---|
| A combo product variant declared with no combo component | Owner creates it | Refused - a combo variant must declare at least one component |
| A variant with combo / add-on / FBT bundles | Owner creates it; any sub-step fails | The whole variant create rolls back; no bundle row is persisted |
OPT - Product Options Built
Feature ID: products/OPT · Phase: P2 · PRDs: PRD-OPT-001 · PRD-OPT-002 · Dev: @nx/commerce
What it does for users: owners define the options a product varies by (Size, Ice level, Colour…) with ordered values, and each variant takes one value per option - so each variant is uniquely identified by its combination of choices. At product creation the owner declares the option axes together with the exact variants to generate, and the system creates the whole product - options, values, variants, bindings, identifiers and inventory attributes - atomically, resolves one default variant, and makes each variant findable by its options in search.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-OPT-001 | M | Owner can define a product's options, each with a name and a display order | Built |
| URD-OPT-002 | M | Each option carries an ordered list of values | Built |
| URD-OPT-003 | M | An option and its values are created or changed together in one step | Built |
| URD-OPT-004 | M | A variant declares its value on each option it uses; an unknown option or value is refused | Built |
| URD-OPT-005 | M | A variant takes at most one value per option | Built |
| URD-OPT-006 | M | Every value a variant uses must belong to the product and to its option | Built |
| URD-OPT-007 | M | No two variants of a product may share the same combination; option-free variants are exempt | Built |
| URD-OPT-008 | M | A merchant setting can require every variant to use options, refusing option-free ones | Built |
| URD-OPT-009 | S | An owner can change which options a variant uses | Built |
| URD-OPT-010 | S | An option offered with no values, or a duplicated choice, is refused | Built |
| URD-OPT-011 | M | At product creation the owner supplies both the option axes and the exact variants to generate, each declaring its own choice per axis | Built |
| URD-OPT-012 | M | Only the declared variants are generated - the system never auto-expands the full option matrix | Built |
| URD-OPT-013 | M | Option axes, their values, and every declared variant are generated together with the product in one atomic step | Built |
| URD-OPT-014 | M | Exactly one generated variant is the default: an explicitly flagged one, else the first declared is promoted | Built |
| URD-OPT-015 | M | Declaring more than one explicit default in the same creation request is refused | Built |
| URD-OPT-016 | S | The owner can change which variant is the default after creation | Built |
| URD-OPT-017 | S | Each generated variant can carry its own SKU / barcode; a barcode repeated in-request or already used in the merchant is refused | Built |
| URD-OPT-018 | S | Inventory-tracking attributes are generated only for stockable variant types; combos and non-stockable types are created untracked | Built |
| URD-OPT-019 | S | Generated variants and their option choices are denormalized into search, kept current as axes, values, or bindings change | Built |
Acceptance
AC-OPT-01: Define options and declare a variant
| Given | When | Then |
|---|---|---|
| A product with options Size (S/M/L) and Ice (100/50/0) | Owner declares a variant for Large, 50% ice | Variant is saved with its two chosen values |
| The same product | Owner declares a variant using a size that doesn't exist | Refused - the value is not part of the product |
AC-OPT-02: Duplicate combination refused
| Given | When | Then |
|---|---|---|
| A variant for Large, 50% ice already exists | Owner declares another variant for the same Large, 50% ice | Refused - the combination is the same regardless of the order chosen |
AC-OPT-03: Strict-options setting
| Given | When | Then |
|---|---|---|
| The strict-options setting is on | Owner creates a variant that uses no options | Refused - every variant must use options |
| The setting is off | The same request | Variant saved; option-free variants may coexist |
AC-OPT-04: Choices stay within the product
| Given | When | Then |
|---|---|---|
| An option belonging to product A | A variant of product B tries to use it | Refused - the option is not part of that product |
| A value belonging to the Size option | A variant pairs it with the Ice option | Refused - the value is not part of that option |
AC-OPT-05: Explicit variant generation
| Given | When | Then |
|---|---|---|
| A product with axes Size (S/M/L) and Ice (100/50/0) | Owner creates it declaring five specific variants | Exactly those five variants are generated - never the full nine-cell matrix |
| The same create | Any sub-step fails (bad option, duplicate barcode, two defaults) | The whole product graph rolls back; nothing is persisted |
AC-OPT-06: Default variant on generation
| Given | When | Then |
|---|---|---|
| Several declared variants, one flagged default | Product is created | The flagged variant is the default |
| Several declared variants, none flagged | Product is created | The first declared variant is promoted to default |
| Two declared variants flagged default | Product is created | Refused - only one default is allowed |
AC-OPT-07: Change the default after creation
| Given | When | Then |
|---|---|---|
| A product whose default is variant A | Owner promotes variant B to default | B becomes the default and A is no longer default |
AC-OPT-08: Per-variant barcode uniqueness
| Given | When | Then |
|---|---|---|
| Two declared variants carry the same barcode | Product is created | Refused - the barcode is duplicated within the request |
| A declared variant's barcode already exists in the merchant | Product is created | Refused - the barcode is already in use |
AC-OPT-09: Inventory attributes by variant type
| Given | When | Then |
|---|---|---|
| A stockable variant type | Variants are generated | Inventory-tracking attributes are generated for each variant |
| A combo or non-stockable type | Variants are generated | Variants are created untracked |
AC-OPT-10: Variants findable by their options
| Given | When | Then |
|---|---|---|
| A generated variant bound to Size=M, Ice=50 | Search is queried/filtered by its option values | The variant is returned and filterable by size and ice |
| Its axis, value, or binding later changes | The change is applied | The variant's option facets in search are refreshed accordingly |
FAR - Fares / Pricing Built
Feature ID: products/FAR · Phase: P2 · PRDs: - · Dev: @nx/pricing
What it does for users: every variant carries exactly one fare set with at least one default fare; fares can be tiered by date range, quantity window, and context (channel, time), and the system resolves the applicable price at sale time (override → discount → default).
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-FAR-001 | M | Every variant has exactly one fare set | Built |
| URD-FAR-002 | M | Every fare set contains at least one (default) fare | Built |
| URD-FAR-003 | M | Fare amount must be zero or positive | Built |
| URD-FAR-004 | M | At sale time the system resolves the applicable fare: override → discount → default | Built |
| URD-FAR-005 | S | Fares can have effective date ranges | Built |
| URD-FAR-006 | S | Fares can have min / max quantity windows | Built |
| URD-FAR-007 | S | Fares support parent-child groups with context rules (channel, time, quantity) | Built |
The current fare model uses OVERRIDE and DISCOUNT group types (an earlier draft labelled these SALE/OVERRIDE). See pricing fares.
Acceptance
AC-FAR-01: Fare resolution
| Given | When | Then |
|---|---|---|
| A variant with multiple active fares | Added to cart | Override group wins; otherwise lowest qualifying discount; otherwise the default fare; date & quantity windows filter candidates first |
| No qualifying fare | The default (base) fare is used |
PID - Identifiers Built
Feature ID: products/PID · Phase: P1 · PRDs: - · Dev: @nx/commerce
What it does for users: products and variants are looked up by scheme-based codes - SYSTEM and SLUG are auto-generated, while owners can attach SKU, BARCODE, and QRCODE identifiers, each unique per scheme within a merchant.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-PID-001 | M | Each identifier is unique per scheme within a merchant | Built |
| URD-PID-002 | M | A product / variant can hold multiple identifiers across schemes | Built |
| URD-PID-003 | M | SYSTEM and SLUG identifiers are auto-generated on creation | Built |
| URD-PID-004 | S | Owner can assign SKU, BARCODE, QRCODE identifiers | Built |
Acceptance
AC-PID-01: Identifier lookup
| Given | When | Then |
|---|---|---|
| A product created | On creation | SYSTEM and SLUG identifiers are auto-generated |
| Owner assigns a SKU/BARCODE/QRCODE | Saved | Identifier is unique per scheme within the merchant |
CMP - Promotions In-progress
Feature ID: products/CMP · Phase: P3 · PRDs: - · Dev: @nx/pricing
What it does for users: owners create and manage merchant-scoped promotion campaigns with a date range and usage limit; the automatic discount application at pricing time is not yet enabled.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-CMP-001 | S | Owner can create and manage promotion campaigns (with date range, usage limit) | In-progress |
| URD-CMP-002 | S | Promotions are scoped to a merchant | In-progress |
| URD-CMP-003 | C | Promotion discounts are applied automatically at pricing time | Planned |
Promotion CRUD is functional; the discount calculation engine is disabled, so discounts are not yet applied automatically. See pricing promotions.
Acceptance
AC-CMP-01: Promotion campaign
| Given | When | Then |
|---|---|---|
| Owner with an active merchant | Creates a promotion campaign with a date range and usage limit | Campaign created and scoped to the merchant |
ACC - Access Control Built
Feature ID: products/ACC · Phase: P1 · PRDs: - · Dev: @nx/commerce
What it does for users: every product operation is filtered by the requesting user's role - owners and employees see only products in their own/assigned merchants, while admins bypass role filtering.
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-ACC-001 | M | All product operations are filtered by the requesting user's role | Built |
| URD-ACC-002 | M | Owner sees only products under own merchants | Built |
| URD-ACC-003 | M | Employee sees only products in assigned merchants | Built |
| URD-ACC-004 | M | Admin / Super Admin bypass role filtering | Built |
Acceptance
AC-ACC-01: Role-based access
| Given | When | Then |
|---|---|---|
| Owner with merchants X, Y | Views products | Only products under X and Y are returned |
| Employee assigned to X | Views products | Only products under X are returned |
| Admin | Views products | All products returned; filtering bypassed |
SCH - Sale Channels (Menu organizer & channel visibility) Built
Feature ID: products/SCH · Phase: P2 · PRDs: PRD-SCH-001 · Dev: @nx/commerce
What it does for users: owners organize what they sell into sale channels - named, multilingual, hierarchical menu surfaces (counter, takeaway, delivery, kiosk). A product appears in a channel only when assigned to it, so visibility is a single switch flipped at product creation. Each channel carries a priority-ordered inventory-location chain (the lowest priority is the auto-pick default stock source), has a lifecycle status (activated / deactivated / archived), and cannot be deactivated while it still has active orders. Every channel and product list is served filtered by the caller's policy grants - an owner sees only their merchants' channels, an admin sees all - so the menu a device receives is always scoped to who is asking. (This is the detailed feature behind the URD-PRD-014 sale-channel mention.)
Requirements
| ID | P | Requirement | Status |
|---|---|---|---|
| URD-SCH-001 | M | Owner can create sale channels within a merchant, each with a multilingual name/description and an auto-generated system identifier | Built |
| URD-SCH-002 | M | A channel slug is unique among live channels within the same merchant | Built |
| URD-SCH-003 | M | A channel has a lifecycle status: activated, deactivated, archived | Built |
| URD-SCH-004 | M | Owner can update a channel; an update with no fields is refused | Built |
| URD-SCH-005 | M | Deactivating a channel is refused while it still has active sale orders | Built |
| URD-SCH-006 | S | Channels support a parent-child hierarchy | Built |
| URD-SCH-007 | M | A product is assigned to one or more channels; the assignment controls whether the product appears in that channel | Built |
| URD-SCH-008 | M | Product-to-channel assignments are written as part of the atomic product create | Built |
| URD-SCH-009 | S | A channel carries a priority-ordered inventory-location chain; the lowest-priority location is the auto-pick default; the chain diff-syncs on update | Built |
| URD-SCH-010 | M | Deleting a channel unlinks its products and soft-deletes the channel | Built |
| URD-SCH-011 | M | Channel and product lists/counts are served filtered by the caller's policy grants; admins bypass the filter | Built |
| URD-SCH-012 | S | Channels and products are searchable, scoped to the caller's merchants | Built |
| URD-SCH-013 | S | Creating a merchant's first channel completes the merchant's channel onboarding step | Built |
Acceptance
AC-SCH-01: Create a channel
| Given | When | Then |
|---|---|---|
| Owner with an active merchant | Creates a channel with a multilingual name and a slug | Channel created, scoped to the merchant, with an auto-generated system identifier |
| A live channel in the merchant already uses that slug | Owner creates another with the same slug | Refused - the slug is already in use in this merchant |
AC-SCH-02: Channel visibility via product assignment
| Given | When | Then |
|---|---|---|
| A Counter channel and a Delivery channel | Owner creates a product assigned only to Counter | The product appears in Counter and not in Delivery |
| The same product | Owner assigns it to Delivery as well | The product now appears in both channels |
AC-SCH-03: Deactivation guard
| Given | When | Then |
|---|---|---|
| A channel with an open (active) order | Owner deactivates the channel | Refused - the channel still has active orders |
| The same channel after its orders are closed / cancelled | Owner deactivates it | The channel is deactivated |
AC-SCH-04: Inventory-location chain
| Given | When | Then |
|---|---|---|
| A channel created with locations [A, B, C] | The channel is saved | Location A (lowest priority) is the auto-pick default stock source |
| The chain is updated to [B, A] | The update is applied | B becomes the default; removed locations are unlinked; survivors keep their row |
| The input lists a location twice | The channel is saved | The duplicate collapses to its first occurrence |
AC-SCH-05: Policy-scoped serving
| Given | When | Then |
|---|---|---|
| Owner granted merchants X and Y | Lists channels / products | Only channels / products of X and Y are returned |
| Employee assigned to X | Lists channels / products | Only those of X are returned |
| Admin / Super Admin | Lists channels / products | All are returned; filtering is bypassed |
AC-SCH-06: Delete unlinks products
| Given | When | Then |
|---|---|---|
| A channel with assigned products | Owner deletes the channel | The channel's product assignments are unlinked and the channel is soft-deleted |
7. Constraints & Non-Goals
Constraints
| ID | Constraint |
|---|---|
| C-01 | A product belongs to exactly one merchant |
| C-02 | Product slug is unique within the same merchant |
| C-03 | Every product has at least one variant |
| C-04 | Every variant has exactly one fare set with at least one fare |
| C-05 | Aggregate create / update operations are all-or-nothing |
| C-06 | All records use soft-delete; nothing is physically removed |
| C-07 | Role-based filtering applies to every list and count operation |
Non-Goals
- Bill of Materials / recipe management (→ Inventory)
- CSV / Excel bulk import
- Unit-conversion engine
- Promotion discount calculation (CRUD only today)
8. Version History
| Date | Author | Description | Ver |
|---|---|---|---|
| 2026-02-26 | P. Do - Product Owner | Initial user stories & requirements | v0.1 |
| 2026-04-16 | P. Do - Product Owner | Expanded variant & fare requirements | v0.3 |
| 2026-05-29 | Docs migration | Restructured to module-docs convention; status-honest Built/In-progress/Planned per dev docs; promotions split from inventory (moved to Inventory module) | v0.4 |
| 2026-06-04 | Claude (AI pair) | Reorganize by feature (Feature Spine); each feature carries its own requirements + acceptance | v0.5 |
| 2026-06-15 | Product squad | Add BND Combos & Bundles feature (URD-BND-001..016) with acceptance; links PRD-BND-001 | v0.6 |
| 2026-06-15 | Product squad | Add SCH Sale Channels (menu organizer & channel visibility) feature (URD-SCH-001..013) with acceptance; links PRD-SCH-001 | v0.7 |