PRD: Device shift context & printer binding
| Module | Device (CORE-04) | PRD ID | PRD-DSX-001 |
| Status | Shipped | Owner | Sale squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/sale · @nx/commerce · @nx/core | URD | DSX · DEV · POS · PRN |
TL;DR
Makes the registered device the runtime anchor of a sales station. Every shift action carries the device in the
x-device-infoheader; the platform resolves that device, derives the merchant from the device record (never trusting a client-supplied merchant), and enforces tenant access before anything runs. Opening a shift opens or joins the single open shift for the device's sale channel - concurrent opens converge on one shift, never a duplicate. A per-device cash drawer is created only for cash-drawer-enabled devices, and the current-context call reports who is participating (active, joined, pending devices) plus expected cash unless blind-count is on. Print jobs route to the printer bound per kitchen station (printer, copies, auto-print). The result: a device is registered once against a merchant, and shifts, drawers, reports, and receipts all bind to that one anchor.
1. Context & Problem
A merchant runs several physical stations - POS terminals, mobile apps, a back counter - against one merchant. The device registration increment (PRD-DEV-001) gives each station a record and a stable identity, but it does not define how that record becomes the operational anchor a shift, a cash drawer, a report, and a printer all hang off.
Without that anchor, a client would have to tell the server which merchant and which shift it is acting in - so two terminals could open two shifts for the same channel, a cash drawer could be created for a device that has no drawer, and a print job would have nowhere to route. Trusting a client-supplied merchant also breaks tenant isolation.
This increment closes the gap: the x-device-info header resolves every shift action to a device, the merchant is derived from the device row, the channel's shift is opened or joined (never duplicated), drawers and participation track per device, and a print destination is bound per station. The station's identity, established at registration, now governs its whole runtime.
2. Goals & Non-Goals
Goals
- Resolve every device-scoped shift action from the
x-device-infoheader, deriving the merchant from the device record and enforcing tenant access. - Open-or-join the single open shift per sale channel - concurrent opens converge on one shift.
- Enroll the opening device and check in the opening employee; create a per-device cash drawer only for cash-drawer-enabled devices.
- Return a device participation view (active / joined / pending devices) and expected cash, omitting expected cash under a blind-count policy.
- Generate X (mid-shift) and Z (end-of-shift) reports per device and per shift, keyed on the resolved device.
- Keep the device a merchant-scoped, authenticated, search-isolated record; bind a printer per station (printer, copies, auto-print) and carry the cash-drawer flag on the device.
Non-Goals
- Device registration, lifecycle, and the back-office fleet surface - specified in PRD-DEV-001 (URD-DEV).
- The ESC/POS print engine itself - rasterization, code page 28, paper widths - specified under PRN.
- Cash-reconciliation math, blind-count tolerance, and report section contents - they are consumed here, not specified here.
- Heartbeat / health monitoring and remote deactivation (MON).
- POS Point hardware certification (POS).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Anchor coverage | 100% of shift actions resolve to a device via x-device-info; merchant is never read from the client |
| No duplicate shifts | One open shift per sale channel even under concurrent opens from multiple devices |
| Drawer correctness | A cash drawer exists only for cash-drawer-enabled devices |
| Participation accuracy | Active / joined / pending device counts match the devices enrolled in the shift |
| Tenant isolation | A device assigned to another merchant cannot act in, or be seen by, this merchant |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Cashier | Open or join the station's shift, drop/pay cash, run an X read, close the shift - all from one device |
| Owner / Manager | See which devices are participating in a channel's shift and review device/shift Z reports |
| Back-office | List shifts for a merchant without a device, using the active-merchant scope |
Core scenario: a cashier signs in on a registered POS terminal and opens the lunch shift for the dine-in channel. The header carries the device; the platform derives the merchant from the device record, finds no open shift for the channel, and creates one - then enrolls the device and checks in the cashier. Because the device is cash-drawer-enabled, a drawer is created with the opening float. A second terminal on the same channel opens and joins the same shift; the current-context call now shows two joined devices. At close, the device runs a Z report keyed on its own record. A second merchant's terminal never appears in either list.
5. User Stories
- As a cashier, I open a shift from my terminal without telling the system which merchant I am, so I cannot act in the wrong tenant.
- As a cashier, when a teammate already opened the channel's shift, my open joins it instead of creating a second one.
- As a cashier, my drawer is created only because my device drives a cash drawer, so a drawer-less device never gets a phantom drawer.
- As a manager, I see which devices are active, joined, and still pending on a channel's shift.
- As a manager, I run X and Z reports for a single device or the whole shift.
- As back-office, I list a merchant's shifts without holding a device, using the merchant scope.
- As an owner, I bind each kitchen station to its printer with a copy count and auto-print, so tickets route to the right printer.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Every device-scoped shift action resolves its device from the x-device-info header and derives the merchant from that device record | URD-DSX-001 |
| FR-2 | A missing header on a device-scoped action is rejected; back-office reads may run without a device via the active-merchant scope | URD-DSX-002 |
| FR-3 | The device must exist, be assigned to a merchant, and match any supplied merchant; otherwise the action is rejected | URD-DSX-003 |
| FR-4 | Tenant access is enforced on the resolved merchant for every shift action | URD-DSX-004 |
| FR-5 | Opening a shift opens or joins the single open shift for the device's sale channel; concurrent opens converge on one shift | URD-DSX-005 |
| FR-6 | A sale channel supplied on open must belong to the resolved merchant | URD-DSX-006 |
| FR-7 | The opening/joining device is enrolled and the opening employee checked in alongside it | URD-DSX-007 |
| FR-8 | A per-device cash drawer is created on open only for cash-drawer-enabled devices | URD-DSX-008 · URD-DSX-014 |
| FR-9 | Current context returns shift, enrollment, optional drawer, expected cash, and device participation (active / joined / pending) | URD-DSX-009 |
| FR-10 | Expected cash is omitted when the merchant's blind-count policy is on or there is no drawer | URD-DSX-010 |
| FR-11 | X and Z reports can be generated per device and per shift, keyed on the resolved device | URD-DSX-011 |
| FR-12 | The device is a merchant-scoped, authenticated record; device search is limited to the merchants the user has joined | URD-DSX-012..013 |
| FR-13 | A print destination is bound per kitchen station (printer, copies, auto-print); jobs route to the bound printer | URD-DSX-015 |
Full requirement text and acceptance criteria live in the Device URD - DSX. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Tenancy & authz | Merchant is derived from the device record, never the client; tenant access is asserted on every action; device API access requires JWT or Basic Auth |
| Search isolation | Device search returns only the merchants the signed-in user has joined |
| Concurrency | Open-or-join is race-safe - a concurrent create converges on the single open shift under a row lock, never a duplicate |
| Atomicity | A shift open (shift + enrollment + employee check-in + drawer) is one all-or-nothing transaction |
| Consistency | A drawer exists only for cash-drawer-enabled devices; participation counts reflect enrolled devices |
| i18n | Device name and station name are bilingual ({ en, vi }) |
8. UX & Flows
Key surfaces: the POS open/current/close screens in sale-renderer carry the device header; the current-context panel shows active / joined / pending devices and expected cash; the back-office shift list in apps/client runs under the merchant scope without a device; station-to-printer binding is configured per kitchen station.
9. Data & Domain
| Entity | Role |
|---|---|
Device | The merchant-scoped station record; carries the cash-drawer flag and is the anchor every shift, drawer, and report binds to |
Shift | The open-or-join session per sale channel; one open shift per channel |
ShiftEnrollment | A device's (and employee's) participation in a shift |
ShiftDrawer | The per-device cash drawer, created only for cash-drawer-enabled devices |
KitchenStation | Carries the per-station printer binding (printer, copies, auto-print) that routes print jobs |
Conceptual only - full schema and invariants live in the commerce domain model and the sale package docs.
10. Dependencies & Assumptions
Depends on
- Device registration (PRD-DEV-001, URD-DEV) - the device record and its merchant assignment must exist before it can anchor a shift.
- Sale - shift management (
@nx/sale) - the open-or-join, enrollment, drawer, and report services. - Commerce (
@nx/commerce) - owns theDeviceentity, the cash-drawer flag, and merchant-scoped device CRUD/search. - Sale channels - a channel supplied on open must belong to the resolved merchant.
Assumptions
- The app sends the device in the
x-device-infoheader on every device-scoped shift action. - A device is assigned to exactly one merchant before it transacts.
- The merchant's shift-management and blind-count policies are configured.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Two devices open the same channel concurrently | Open-or-join converges on the single open shift under a row lock; the loser joins, never duplicates |
| Client supplies a wrong or foreign merchant | Merchant is derived from the device record and tenant access is asserted; a mismatched supplied merchant is rejected |
| Drawer created for a drawer-less device | Drawer creation is gated on the device's cash-drawer flag |
| Expected cash leaks under blind count | Expected cash is omitted from the context when blind-count is on or there is no drawer |
| Action runs without a device header | Device-scoped actions reject a missing header; only explicit back-office reads run device-less under the merchant scope |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - DSX in the URD feature catalog, alongside DEV / POS / PRN |
| Rollout | All merchants; no feature flag |
| Migration | Cash-drawer flag added on the device model; no data backfill required |
| Launch criteria | Shift actions resolve the device and derive the merchant; open-or-join yields one shift per channel under concurrency; drawers appear only for cash-drawer-enabled devices; participation and expected-cash (blind-count aware) return correctly; device/shift X and Z reports generate; station print binding routes jobs |
| Monitoring | Duplicate-open rate, header-missing rejection rate, drawer-vs-flag consistency, cross-merchant access attempts |
13. FAQ
Who decides which merchant a shift belongs to? The device does - the merchant is derived from the device record resolved via x-device-info. The client never supplies it for device-scoped actions.
What happens when two terminals open the same channel's shift at once? They converge on one shift - the first creates it, the second joins it. There is never a duplicate open shift per channel.
Does every device get a cash drawer? No - a drawer is created at open only for devices flagged cash-drawer-enabled. Other devices join the shift without a drawer.
Why is expected cash sometimes missing from the context? It is omitted when the merchant's blind-count policy is on, or when the device has no drawer.
Can I run a report without a device? The shift list runs back-office under the merchant scope without a device. Device and shift X/Z reports are keyed on the resolved device and need the header.
How does a ticket reach the right printer? Each kitchen station binds to a printer with a copy count and an auto-print flag; print jobs route to the bound printer.
References
- URD: Device - DSX · DEV · POS · PRN
- Sibling PRDs: Device registration, management & peripherals · Device signal & notifications
- Module: Device - overview + capabilities
- Developer: @nx/sale · @nx/commerce · @nx/core
- Apps: sale-renderer · client