Skip to content

PRD: Multi-employee Shifts

ModuleSalePRD IDPRD-SHF-001
StatusReady to devFEATSHF · BANA-1426
EpicBANA-1321PlaneBANA-1557
Date2026-07-17Versionv0.2
Packages@nx/saleURDSHF
SurfaceSale · POS
OwnerViệt Võ · Phát Nguyễn

TL;DR

  • ✅ Real multi-staff shifts already exist server-side: open/close on device, multiple staff auto-enrolled into the same shift, cash reconciliation (counted vs expected, with a discrepancy tolerance), and X (mid-shift) / Z (close) reports.
  • ✅ A sale channel accepts multiple shifts open in parallel; each device/employee holds exactly one active shift at a time; opening a new shift is never blocked by a prior one.
  • 🔶 Revenue broken down per employee and per period (rolled up across shifts) is not built - reports today are per-drawer/device and per single shift.
  • 🚧 The mandatory discrepancy note (URD-SHF-008, Must) isn't enforced by the backend yet - the notes field is optional on every reconcile call.
  • 🚧 The POS still runs the legacy single-user session - the remaining work in this increment is wiring the FE onto the new shift system and verifying it end-to-end; this PRD doubles as the test-case spec.

1. Context & Problem

Phase 1 shipped only a single-user POS session (the PosSession v1 API - now fully removed from the backend). Real merchants need shifts shared by multiple staff (cashier + waiters on one till) with clear per-person accountability, cash reconciliation at the counter, and a printable handover report.

The shift system (Shift v2) is fully built server-side: shifts scoped per sale channel, polymorphic enrolment (device + employee), per-device cash drawers, cash movements (each posting a finance voucher), and X/Z report generation - complete backend logic, not schema stubs. What's left is purely front-end: the POS doesn't call the new shift system yet, and there was no spec detailed enough to write test cases against when wiring the FE over. This document closes both gaps.

2. Goals & Non-Goals

Goals

  • Open/close a shift on device, surviving app restarts (URD-SHF-001).
  • Multiple employees (not just multiple devices) enrol into one shift, automatically (URD-SHF-002).
  • Mid-shift X report (repeatable) and close/reconcile Z report (URD-SHF-003).
  • Cash reconciliation at drawer close: counted vs expected, discrepancy recorded (URD-SHF-004).
  • Sales within a shift attribute to the acting employee via a presence record (URD-SHF-005).
  • Cash drawer movements - pay-in/pay-out/safe-drop, each posting a finance voucher (URD-SHF-006).
  • Owners see revenue per shift (URD-SHF-007 - the "per shift" half only; see 🔶 in FR-007).
  • Closing a drawer with a nonzero discrepancy requires a reason note (URD-SHF-008).
  • The opening float can't be self-edited at the POS once a drawer is open (URD-SHF-009).

Non-Goals

  • Per-member capability flags within a shift (e.g. refund/void permissions scoped to individual staff) - tracked separately under Permissions, not in this PRD.
  • Payroll / hourly timesheets.
  • Revenue per period (rolled up across shifts over a date range) and per employee - no aggregation endpoint exists yet; see 🔶 in FR-007. This is the unfinished half of URD-SHF-007, not a permanent non-goal.
  • The X/Z report's JSON structure, payment-method/category breakdowns, and read-access gating - owned by PRD: Shift Reports (X/Z); this PRD only describes the sale-side events that feed that report, not its structure.
  • A PIN step to access reports - owned by the Shift Reports (X/Z) PRD, not this one.

3. Success Metrics

MetricTarget
POS runs the new shift systemOld single-user session fully replaced, no regression on the sales flow
Z reportExpected-cash formula verified against a real day of trading at a pilot merchant
Discrepancy notes🚧 100% of reconciles with a nonzero discrepancy carry a reason note - depends on making the note mandatory in FE or backend (see FR-008)
Correct staff attribution0 presence/handover records attributed to the wrong employee at the pilot merchant (requires each staff member to log in with their own account)

4. Personas & Use Cases

PersonaGoal in this feature
CashierOpens/closes the shift, counts the till, handles the discrepancy dialog, prints the handover slip
WaiterAuto-enrols the moment they sell on a shared device, no separate action needed
Owner / ManagerReads X/Z reports per shift; adjusts an opening float or a reconciled result on a drawer when needed (with history)

Core scenario: a shop shares one till between two devices (the counter register and a handheld for waiters). The cashier opens the shift and enters the opening float for that till; the handheld device joins the open shift. Through the shift, both devices sell - whoever is logged into a device auto-enrols into that device's shift. At close, the cashier closes the shift (locking sales everywhere), counts the cash in the till, and the system reports expected cash and the discrepancy; if it's outside tolerance, the cashier recounts or explicitly acknowledges it. Once reconciled, the shift finalizes and a printable Z handover slip is available.

5. User Stories

  • As a cashier, I open the shift with a float, my colleagues enrol and start selling without me doing anything else, and at close I count the till and see the discrepancy immediately.
  • As a waiter, I log into my own account on a shared sales device and start selling right away - I don't need to know who opened the shift or call any enrolment action.
  • As a cashier, when the counted cash is off by more than the allowed tolerance, I'm asked to recount or explicitly confirm I know about and accept the discrepancy before the close completes.
  • As an owner, I read the Z report for each closed shift and trust the number is final (locked).
  • As an owner, if an opening float was entered wrong and the drawer already closed, I can correct it and still see the old value alongside the new one.
  • As a cashier, when my shop runs multiple tills, I reconcile them one at a time at close, and the shift only truly finalizes once the last till is reconciled.

6. Functional Requirements

#RequirementStatusURD ref
FR-001Open/close a shift on device.
• The first device opens a new shift for a sale channel; later devices join the shift already open for that channel. A sale channel accepts multiple shifts open in parallel; each device holds exactly one open shift at a time.
• A device with a cash drawer enabled must enter an opening float before it can sell; a device without one skips this step.
• Close is initiated by any device in the shift and locks sales for the entire shift (every device/employee in it, not just the one that closed it).
• Lifecycle: Open → Closed (awaiting reconciliation) → Reconciled (once the last drawer is reconciled; a shift with no drawers finalizes immediately on close).
Opening a new shift is never blocked by a prior one: in-flight orders on the device carry over to the new shift, completed orders keep their original shift; the prior shift does not need to be reconciled first. Cash integrity is checked at the drawer level on open (see FR-004).
• Shift state survives app restarts.
URD-SHF-001
FR-002Multi-employee enrolment (automatic).
• Beyond devices, the system separately enrols employees into a shift - the FE never calls a dedicated enrolment action: an employee opening/joining a shift is enrolled immediately; another employee logging into a device already in the shift and selling for the first time is auto-enrolled (lazy enrolment).
• An employee switching to a device in a different shift auto-leaves the old shift and joins the new one - presence tracks the most recent device worked on.
• When a shift finalizes ("Reconciled"), every employee still present is automatically checked out.
• Employee identity comes from the login on the device - each employee must log in with their own account, no shared logins, for presence/handover data to attribute correctly.
• Presence tracking is best-effort: an enrolment failure never blocks a sale.
• Each device/employee holds at most one active enrolment at a time.
URD-SHF-002
FR-003X (mid-shift) and Z (close) reports.
• X report: a live snapshot of the open shift, repeatable any number of times, never mutates the shift.
• A Z report is finalized when a drawer is reconciled: each drawer produces one Z report per device attached to it (not a single pooled Z) - when several devices share a drawer, each Z carries the same pooled cash figures (flagged "cash shared") plus that device's own sales.
• A device without a drawer gets its Z cut the moment the shift closes - no need to wait for reconciliation since there's no cash to count.
• Once the shift's last drawer is reconciled (or the shift has no drawers at all), the shift finalizes and one more shift-wide aggregate Z report is produced (cash fields = 0, since those are already covered by the per-device Z's; sales figures cover the whole shift) - still the source of truth for cash, summing the physical drawers.
• Both X and Z can be saved as a point-in-time snapshot for later review.
• The report's data structure, payment-method/category breakdowns, and read-access rules belong to PRD: Shift Reports (X/Z) - this FR only describes the shift-side events that produce the report.
URD-SHF-003
FR-004Cash reconciliation at drawer close.
• The cashier enters the actual counted cash (required), optionally with counted non-cash amounts for Bank transfer / QR / Mobile POS.
• Expected cash = opening float + cash sales − cash refunds + pay-ins − pay-outs; discrepancy = counted − expected.
• If the merchant has "blind count" on (the default), the cashier does not see the expected amount before entering the count - only after reconciling.
• If |discrepancy| exceeds the merchant's configured tolerance (default 0), the system rejects the close and requires either a recount or an explicit acknowledgement of the discrepancy before proceeding.
• Each device's drawer reconciles independently (not one reconciliation for the whole shift's cash); at reconcile time, the cashier chooses whether that device drops out of shift participation or carries over into the next shift (still requiring a fresh opening float for the next shift).
• Cash integrity is checked at the drawer level on opening a drawer (not on opening a shift): a drawer left "Closed - not reconciled" by a prior shift is rejected until it's reconciled.
• An owner/manager (no device required) can correct a drawer's reconciled result afterward; every correction is saved as a version, with full history available.
URD-SHF-004
FR-005Sales attribute to the acting employee.
• A sale attributes to the shift of the device that opened/closed it: an order started under shift A and paid after the device moved to shift B counts toward shift B (already supported), not shift A.
• The acting employee (order creation / checkout) is derived from the shift's presence record at the moment of the action (see FR-002), not a direct "sold by" field stamped on the order.
• 🔶 Partial: a revenue breakdown per individual employee is not built - reports today roll up by drawer/device only, not by acting staff (see FR-007).
🔶URD-SHF-005
FR-006Cash drawer movements.
• Three movement types, recorded per drawer: pay-in (cash added), pay-out (cash removed), safe-drop (cash pulled/banked) - all require an amount, with an optional reason.
• Every movement posts a finance voucher (receipt/payment) into the merchant's finance accounts - not just an internal log entry.
• Movements are only allowed while the drawer is open; once the shift closes, the drawer accepts no more movements.
URD-SHF-006
FR-007Revenue by shift and by period.
• Revenue per shift: ✅ available via the X/Z reports and the shift report bundle (shift-wide + per-device), plus a filterable shift list by channel/open-close window.
• 🚧 Revenue per period (rolled up across shifts over a date range, e.g. "this week's revenue") and per employee - not built; there is currently no report that aggregates across shifts or breaks down by acting staff. This is the unfinished half of URD-SHF-007, not all of it.
🔶URD-SHF-007
FR-008Reason note on a nonzero cash discrepancy.
• When reconciling a drawer produces a discrepancy (counted ≠ expected), the cashier enters a reason note before the reconcile completes.
• 🚧 Forward-spec: the notes field is currently optional on every reconcile call, discrepancy or not - making it mandatory when the discrepancy is nonzero (in the FE at close, or as a backend validation) is remaining work in this increment to satisfy URD-SHF-008 (Must).
🚧URD-SHF-008
FR-009Opening float lock / correction.
• The opening float locks at the POS: there is no flow for a cashier to self-edit the float once a drawer has been opened.
• 🔶 Not an absolute lock: an owner/manager can correct a drawer's opening float afterward via a separate back-office action (no device required) - every correction is saved with history (old value → new value) available for review.
🔶URD-SHF-009

6.1 Acceptance Criteria

  • Device 1 opens a shift for channel X, entering a 500,000₫ float for its own drawer → shift becomes "Open"; device 2 joins the same shift, entering its own float for its own drawer → both devices belong to one shift. (FR-001)
  • Employee A logs into device 1 and opens the shift → A is enrolled immediately. Employee B logs into device 2 (already in the shift) and creates their first sale → B is auto-enrolled at that moment, with no separate enrolment action. (FR-002)
  • While the shift is open, requesting the X report repeatedly → each call returns the latest snapshot; none of them change shift state or create a Z record. (FR-003)
  • A drawer shared by device 1 and device 2 gets reconciled → the system returns two Z reports (one per device), with identical cash figures across both and different sales figures per device. (FR-003)
  • Closing a shift with 2 drawers → reconciling drawer 1 alone does not finalize the shift; only after drawer 2 (the last one) is reconciled does the shift move to "Reconciled" and produce one additional shift-wide aggregate Z report. (FR-001, FR-003)
  • A cashier closes a shift and counts cash 15,000₫ off from expected, while the merchant's tolerance is 10,000₫ → the system rejects completion, requiring either a recount or an explicit acknowledgement; only after acknowledging does the reconcile succeed. (FR-004)
  • With "blind count" enabled, the cashier's reconcile screen does not show the expected cash figure before the actual count is entered. (FR-004)
  • A device still has a drawer in "Closed - not reconciled" from a prior shift → opening that drawer for a new shift is rejected until it's reconciled; meanwhile, opening a new shift on the channel still succeeds normally, unblocked by the pending drawer. (FR-001, FR-004)
  • An owner reopens a drawer reconciled 2 days ago and corrects its opening float → the new value is recorded, and the original value remains visible in history for comparison. (FR-009)
  • 🚧 A cashier reconciles a drawer with a nonzero discrepancy and enters no note → today the system still allows completion (no block); once FR-008's validation lands, the expected behavior is that the system requires a note before completion. (FR-008)
  • 🚧 An owner looks for a weekly revenue report rolled up across shifts, or a report for one specific employee → neither exists today; only per-shift revenue is available. (FR-007)

7. Non-Functional Requirements

AspectRequirement
DurabilityShift state survives app restarts; no loss of active enrolments or open drawers
Blocking sales outside a shiftNo sale can occur outside an open shift, when the merchant has shift management enabled (enableShiftManagement, off by default)
Toggling mid-shiftDisabling shift management mid-shift only blocks opening new shifts; an already-open shift runs its full course (selling, cash movements, close, reconcile, X/Z reports) - toggling the setting never strands an open shift
TenancyEvery shift, enrolment, drawer, and movement scopes strictly to its merchant + sale channel; no cross-merchant data leakage
Cash precisionCash and monetary amounts are tracked at 4-decimal precision
Report idempotencySaving a snapshot identical to the most recent one is a no-op - no duplicate records
Best-effort enrolmentEmployee enrolment/presence-tracking failures never block a sale

8. UX & Flows

One shift, two devices, two drawers

Shift handover screen (closing from the X report)

9. Data & Domain

ConceptRole
ShiftA selling session scoped to a sale channel, with lifecycle Open → Closed → Reconciled; a channel can have multiple shifts open in parallel
EnrollmentPolymorphic - represents either a device or an employee within a shift; each device/employee holds one active enrollment at a time
DrawerAttached to the enrollment of a device with cash drawer enabled; carries opening float, expected cash, counted cash, discrepancy
Enrollment eventThe ledger of every action: device drop-in/drop-out, employee check-in/check-out, drawer open, pay-in/pay-out/safe-drop, reconcile
Shift report (X/Z)Mid-shift snapshot (X) or a locked close-out record (Z); one Z per device attached to a drawer, plus one shift-wide aggregate Z - structure detail owned by PRD: Shift Reports (X/Z)
Sale orderAttaches to a shift via the enrollment that opened it / the enrollment that closed it (payment); revenue counts toward the shift that closed the order

Conceptual only - full detail and invariants live in the sale domain model and the Shift v2 developer doc.

10. Dependencies & Assumptions

Depends on

DependencyWhy
Acting-staff identification via device login (BANA-1140, in flight)Auto-enrolment (FR-002) and sale attribution (FR-005) both need to know who's operating which device
Merchant finance accountsDrawer movements (FR-006) post receipt/payment vouchers into finance accounts
Per-device cash drawer configurationOnly drawer-enabled devices require a float and take part in reconciliation
PRD: Shift Reports (X/Z)Owns the data structure and read-access rules for the X/Z reports that FR-003 produces

Assumptions

  • Each employee logs in with their own account on a shared device - no shared logins - so presence/handover data attributes correctly (FR-002).
  • The merchant picks a drawer policy that fits their layout (one shared till across devices, or one till per device) before enabling shift management; this PRD supports both models but doesn't mandate a default (see Risks §11).
  • The shift policy defaults (enableBlindCount = on, discrepancyTolerance = 0) are workable for the initial pilot merchant; revisit if real operation shows otherwise.

11. Risks & Open Questions

Risk / questionMitigation / status
Cutover: merchants mid-shift when this rolls outDefine the upgrade path - close all legacy PosSession v1 sessions before a merchant moves to the new shift system; the old API is removed, so no side-by-side run is possible
Drawer policy variant (shared till vs per-device)Confirm the v1 default before pilot launch - it directly shapes the multi-drawer reconcile UX
Mandatory discrepancy note (FR-008, URD-SHF-008 Must) not yet enforcedDecide whether to enforce in FE (validate before submit) or backend (conditional required schema) before calling this increment done
Default tolerance (0₫) may be too strict for real operationWatch the rate of shifts requiring a discrepancy acknowledgement during pilot; adjust if it's unusually high

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 (July, WK27-31) - SHF in the URD feature catalog
RolloutFE migration + end-to-end verification at a pilot merchant before broader rollout
MigrationClose every open PosSession v1 session before a merchant moves to the new shift system; no legacy shift data to migrate (new concept)
Launch criteriaAC-SHF-01 (URD) passes at the pilot merchant: a full shift with two employees, Z report matching the expected-cash formula; the legacy session is fully retired
MonitoringRate of shifts requiring a discrepancy acknowledgement, shifts stuck "Closed - not reconciled" too long, employee-enrolment failure rate (best-effort, but worth watching)

13. FAQ

  • Is the backend being rebuilt? No - it's already built; this is FE wiring + verification, and this PRD is the test reference.
  • Is the discrepancy note mandatory? The URD says Must, but the backend currently leaves it optional - see FR-008; needs a decision on where to enforce it before launch.
  • Can the opening float be edited after a drawer opens? Not at the POS, but an owner/manager can correct it via back-office with full history - see FR-009.
  • What happens to the Z report when one drawer is shared by multiple devices? Each device attached to that drawer gets its own Z (same pooled cash, different sales) - see FR-003.
  • Can I see revenue for a week, or for one specific employee? Not yet - only per-shift revenue is available today; see 🔶 in FR-007.

References

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