Skip to content

PRD: Device shift context & printer binding

ModuleDevice (CORE-04)PRD IDPRD-DSX-001
StatusShippedOwnerSale squad
Date2026-06-15Versionv1.0
Packages@nx/sale · @nx/commerce · @nx/coreURDDSX · 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-info header; 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-info header, 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

MetricTarget / signal
Anchor coverage100% of shift actions resolve to a device via x-device-info; merchant is never read from the client
No duplicate shiftsOne open shift per sale channel even under concurrent opens from multiple devices
Drawer correctnessA cash drawer exists only for cash-drawer-enabled devices
Participation accuracyActive / joined / pending device counts match the devices enrolled in the shift
Tenant isolationA device assigned to another merchant cannot act in, or be seen by, this merchant

4. Personas & Use Cases

PersonaGoal in this feature
CashierOpen or join the station's shift, drop/pay cash, run an X read, close the shift - all from one device
Owner / ManagerSee which devices are participating in a channel's shift and review device/shift Z reports
Back-officeList 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

#RequirementURD ref
FR-1Every device-scoped shift action resolves its device from the x-device-info header and derives the merchant from that device recordURD-DSX-001
FR-2A missing header on a device-scoped action is rejected; back-office reads may run without a device via the active-merchant scopeURD-DSX-002
FR-3The device must exist, be assigned to a merchant, and match any supplied merchant; otherwise the action is rejectedURD-DSX-003
FR-4Tenant access is enforced on the resolved merchant for every shift actionURD-DSX-004
FR-5Opening a shift opens or joins the single open shift for the device's sale channel; concurrent opens converge on one shiftURD-DSX-005
FR-6A sale channel supplied on open must belong to the resolved merchantURD-DSX-006
FR-7The opening/joining device is enrolled and the opening employee checked in alongside itURD-DSX-007
FR-8A per-device cash drawer is created on open only for cash-drawer-enabled devicesURD-DSX-008 · URD-DSX-014
FR-9Current context returns shift, enrollment, optional drawer, expected cash, and device participation (active / joined / pending)URD-DSX-009
FR-10Expected cash is omitted when the merchant's blind-count policy is on or there is no drawerURD-DSX-010
FR-11X and Z reports can be generated per device and per shift, keyed on the resolved deviceURD-DSX-011
FR-12The device is a merchant-scoped, authenticated record; device search is limited to the merchants the user has joinedURD-DSX-012..013
FR-13A print destination is bound per kitchen station (printer, copies, auto-print); jobs route to the bound printerURD-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

AreaRequirement
Tenancy & authzMerchant 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 isolationDevice search returns only the merchants the signed-in user has joined
ConcurrencyOpen-or-join is race-safe - a concurrent create converges on the single open shift under a row lock, never a duplicate
AtomicityA shift open (shift + enrollment + employee check-in + drawer) is one all-or-nothing transaction
ConsistencyA drawer exists only for cash-drawer-enabled devices; participation counts reflect enrolled devices
i18nDevice 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

EntityRole
DeviceThe merchant-scoped station record; carries the cash-drawer flag and is the anchor every shift, drawer, and report binds to
ShiftThe open-or-join session per sale channel; one open shift per channel
ShiftEnrollmentA device's (and employee's) participation in a shift
ShiftDrawerThe per-device cash drawer, created only for cash-drawer-enabled devices
KitchenStationCarries 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 the Device entity, 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-info header 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 / questionMitigation / status
Two devices open the same channel concurrentlyOpen-or-join converges on the single open shift under a row lock; the loser joins, never duplicates
Client supplies a wrong or foreign merchantMerchant is derived from the device record and tenant access is asserted; a mismatched supplied merchant is rejected
Drawer created for a drawer-less deviceDrawer creation is gated on the device's cash-drawer flag
Expected cash leaks under blind countExpected cash is omitted from the context when blind-count is on or there is no drawer
Action runs without a device headerDevice-scoped actions reject a missing header; only explicit back-office reads run device-less under the merchant scope

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - DSX in the URD feature catalog, alongside DEV / POS / PRN
RolloutAll merchants; no feature flag
MigrationCash-drawer flag added on the device model; no data backfill required
Launch criteriaShift 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
MonitoringDuplicate-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

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