PRD: Sale Channels
| Module | Commerce (CORE-03) | PRD ID | PRD-SC-001 |
| Status | Shipped | Owner | Commerce squad |
| Date | 2026-06-01 | Version | v1.0 |
| Packages | @nx/commerce · apps/bo · apps/client | URD | SC |
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
| Metric | Target / signal |
|---|---|
| Channel-scoped config | 100% of terminal/print/configuration lookups keyed by sale-channel-id (no merchant-wide fallback) |
| Onboarding coverage | Every onboarded merchant has at least one default channel with a system identifier |
| Slug integrity | Zero duplicate channel slugs within a merchant |
| Order safety | Zero deactivations of a channel that backs active orders |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Set up selling channels at onboarding; control where each channel draws stock and how it prints |
| Manager | Add/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
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Default channel(s) created during onboarding | URD-SC-001 |
| FR-2 | Channels managed as part of merchant aggregate operations | URD-SC-002 |
| FR-3 | Channels can be batch-created for an existing merchant | URD-SC-003 |
| FR-4 | Channel slug is unique within the same merchant | URD-SC-004 |
| FR-5 | A unique system identifier is generated on creation and is not editable | URD-SC-005 |
| FR-6 | Channels can be deactivated or archived; deactivation is blocked while the channel backs active orders | URD-SC-007 |
| FR-7 | Inventory locations are attachable to a channel (optional on create); a channel draws stock from its linked location(s) | URD-SC-002 |
| FR-8 | Terminal and print-template configuration are carried on the channel; configuration lookups are scoped by sale-channel-id | URD-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
| Area | Requirement |
|---|---|
| Data integrity | Channel + inventory-location links written transactionally inside the aggregate; a partial-unique index enforces one link per (saleChannelId, inventoryLocationId) |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id); gated by commerce sale-channel permissions |
| Consistency | Aggregate create/update applies the merchant smart-update rule (ID-only = delete, ID+data = update, no ID = create) atomically |
| Order safety | A channel that backs active orders cannot be deactivated (validated before status change) |
| i18n | User-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
| Entity | Role |
|---|---|
SaleChannel | The selling-channel record - name, per-merchant unique slug, system identifier, status, terminal/print config |
SaleChannelInventoryLocation | Link row connecting a channel to one or more inventory locations (priority-ordered) |
| Channel configuration | Encrypted/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 / question | Mitigation / status |
|---|---|
| Channel config drift after moving lookups off merchant-id | Lookups consistently keyed by sale-channel-id; merchant-wide fallback removed |
| Duplicate inventory-location links on a channel | Partial-unique index on (saleChannelId, inventoryLocationId) |
| Deactivating a channel mid-sale | Guard blocks deactivation while active orders reference the channel |
| Standalone channel CRUD outside the aggregate | Out of scope; Planned per URD Non-Goals |
| Channel hierarchy (parent-child) | Could-priority (URD-SC-006); deferred |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | Onboarding backfill ensures existing merchants have a default channel; terminal/print config moved onto the channel |
| Launch criteria | Onboarding 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 |
| Monitoring | Channel 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
- URD: Commerce - Sale Channels
- Related PRD: Organization & Merchant · Config & Deletion Policy
- Module: Commerce - overview + traceability
- Developer: @nx/commerce · domain model