Skip to content

PRD: Sale Channels

ModuleCommerce (CORE-03)PRD IDPRD-SC-001
StatusShippedOwnerCommerce squad
Date2026-06-01Versionv1.0
Packages@nx/commerce · apps/bo · apps/clientURDSC

TL;DR

Lets a merchant sell through one or more selling channels (dine-in, takeout, delivery), created automatically at onboarding and managed afterward via the merchant aggregate or batch-added to an existing merchant. Each channel carries its own inventory-location links and terminal/print configuration, a per-merchant unique slug, and a system identifier - and cannot be deactivated while it still backs active orders. The result: channel-level control of where stock is drawn from and how receipts print, without ad-hoc merchant-wide lookups.

1. Context & Problem

A merchant sells through more than one channel - dine-in, takeout, delivery - and each behaves differently: it may draw stock from a different location, print to a different terminal, or use a different receipt template. Commerce already creates default channel(s) atomically at onboarding and manages them inside the merchant aggregate, but the channel was a thin record: inventory location and terminal/print settings lived at merchant scope, so two channels under one merchant could not differ, and configuration lookups were keyed by merchant rather than channel. There was also no guard preventing a channel from being deactivated while it still backed active orders, risking orphaned in-flight sales.

This increment promotes the sale channel to a first-class configuration unit: location links and terminal/print config move onto the channel, configuration lookups are scoped by sale-channel-id, and a deactivation guard protects in-flight orders - with reworked back-office screens so a user can manage all of it through a consistent UI.

2. Goals & Non-Goals

Goals

  • Default channel(s) created during onboarding and managed as part of merchant aggregate operations.
  • Batch-create channels for an existing merchant, with a per-merchant unique slug and a system identifier generated on creation.
  • Attach inventory locations to a channel (optional on create) so a channel draws stock from its own location(s).
  • Carry terminal and print-template configuration on the channel, with configuration lookups scoped by sale-channel-id.
  • Block deactivation of a channel that still backs active orders.
  • Consistent back-office create/edit screens for channel management.

Non-Goals

  • Standalone sale-channel CRUD outside the merchant aggregate - Planned (see Commerce URD Non-Goals).
  • Channel hierarchy (parent-child) - URD-SC-006 is Could-priority and out of this increment.

3. Success Metrics

MetricTarget / signal
Channel-scoped config100% of terminal/print/configuration lookups keyed by sale-channel-id (no merchant-wide fallback)
Onboarding coverageEvery onboarded merchant has at least one default channel with a system identifier
Slug integrityZero duplicate channel slugs within a merchant
Order safetyZero deactivations of a channel that backs active orders

4. Personas & Use Cases

PersonaGoal in this feature
OwnerSet up selling channels at onboarding; control where each channel draws stock and how it prints
ManagerAdd/edit channels for an existing merchant, link inventory locations, manage terminal/print config
Cashier (client)Sell against the correct channel so stock is drawn from the right location and receipts print correctly

Core scenarios: onboarding creates the default channel → owner batch-adds or aggregate-manages more channels → links inventory locations and terminal/print config per channel → deactivates an unused channel (blocked if it backs active orders).

5. User Stories

  • As an owner, I want default channel(s) created during onboarding, so I can start selling immediately.
  • As a manager, I want to batch-create channels for an existing merchant, so I can expand selling channels without re-running onboarding.
  • As a manager, I want each channel to link its own inventory location(s), so a channel draws stock from the right place.
  • As a manager, I want terminal and print-template config on the channel, so receipts print to the correct device with the correct template.
  • As a manager, I want a unique slug per channel within the merchant, so channel identifiers don't collide.
  • As an owner, I want a channel that backs active orders to be protected from deactivation, so I don't orphan in-flight sales.

6. Functional Requirements

#RequirementURD ref
FR-1Default channel(s) created during onboardingURD-SC-001
FR-2Channels managed as part of merchant aggregate operationsURD-SC-002
FR-3Channels can be batch-created for an existing merchantURD-SC-003
FR-4Channel slug is unique within the same merchantURD-SC-004
FR-5A unique system identifier is generated on creation and is not editableURD-SC-005
FR-6Channels can be deactivated or archived; deactivation is blocked while the channel backs active ordersURD-SC-007
FR-7Inventory locations are attachable to a channel (optional on create); a channel draws stock from its linked location(s)URD-SC-002
FR-8Terminal and print-template configuration are carried on the channel; configuration lookups are scoped by sale-channel-idURD-SC-002

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

7. Non-Functional Requirements

AreaRequirement
Data integrityChannel + inventory-location links written transactionally inside the aggregate; a partial-unique index enforces one link per (saleChannelId, inventoryLocationId)
Tenancy & authzAll operations scoped per merchant (x-merchant-id); gated by commerce sale-channel permissions
ConsistencyAggregate create/update applies the merchant smart-update rule (ID-only = delete, ID+data = update, no ID = create) atomically
Order safetyA channel that backs active orders cannot be deactivated (validated before status change)
i18nUser-facing channel labels are bilingual ({ en, vi })

8. UX & Flows

Key screens (in apps/bo): sale-channel list, create, and edit screens with a shared SaleChannelForm / general-information form; the channel selector and inventory-location/terminal binding surface in apps/client at point of sale.

9. Data & Domain

EntityRole
SaleChannelThe selling-channel record - name, per-merchant unique slug, system identifier, status, terminal/print config
SaleChannelInventoryLocationLink row connecting a channel to one or more inventory locations (priority-ordered)
Channel configurationEncrypted/keyed configuration looked up by sale-channel-id

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

10. Dependencies & Assumptions

Depends on

  • Merchant aggregate (URD-MER) - channels are created and managed inside the merchant aggregate.
  • Onboarding (URD-ORG) - default channel(s) are created atomically during onboarding.
  • Inventory locations (Inventory) - a channel links to existing inventory locations.

Assumptions

  • A merchant exists (or is being created in the same aggregate) before channels are managed.
  • Inventory locations exist before they can be linked to a channel.

11. Risks & Open Questions

Risk / questionMitigation / status
Channel config drift after moving lookups off merchant-idLookups consistently keyed by sale-channel-id; merchant-wide fallback removed
Duplicate inventory-location links on a channelPartial-unique index on (saleChannelId, inventoryLocationId)
Deactivating a channel mid-saleGuard blocks deactivation while active orders reference the channel
Standalone channel CRUD outside the aggregateOut of scope; Planned per URD Non-Goals
Channel hierarchy (parent-child)Could-priority (URD-SC-006); deferred

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (foundation) - see URD feature catalog
RolloutAll merchants; no feature flag
MigrationOnboarding backfill ensures existing merchants have a default channel; terminal/print config moved onto the channel
Launch criteriaOnboarding creates a default channel; batch/aggregate create verified; per-merchant slug uniqueness enforced; channel-scoped config lookup verified; deactivation guard blocks channels backing active orders
MonitoringChannel count per merchant, deactivation-block rate, config-lookup errors by sale-channel-id

13. FAQ

Can I manage a channel outside the merchant aggregate? Not in this increment - channels are managed via the merchant aggregate or batch-added to an existing merchant. Standalone channel CRUD is Planned.

Does each channel have its own stock location? A channel can link one or more inventory locations (optional on create), so it draws stock from its own location(s) rather than a single merchant-wide location.

Why can't I deactivate a channel? Because it still backs active orders. The deactivation guard protects in-flight sales; close or reassign those orders first.

Where does terminal/print config live now? On the channel. Configuration lookups are scoped by sale-channel-id, so two channels under one merchant can print differently.

Is channel hierarchy supported? No - parent-child channel hierarchy (URD-SC-006) is Could-priority and not part of this increment.

References

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