Skip to content

0017. Shifts and Z-reports

  • Status: Accepted
  • Date: 2026-10-27

At the end of the day a shop counts its cash drawer and prints a Z-report: what was sold, what tax was charged, how customers paid, and whether the drawer is over or short. A shop may have several devices, and a table opened before a shift change may be paid after it. Each sale has to be counted exactly once, without coordination between devices and without a server.

  • A shift belongs to one device and its cash drawer. It opens with a float, records cash paid in and out with a reason, and closes with the cash counted. Each device numbers its own shifts 1, 2, 3…; the number is the Z-report number.

  • Shifts are events (shift.opened, shift.cash_movement, shift.closed), projected like orders: pure, idempotent, and an event that cannot apply is kept as a conflict.

  • Money is reported where it moved; a sale is reported where it closed. Payments, closes and refunds carry the shiftId of the device’s open shift. A Z-report counts:

    • sales (bill totals, SST per code and rate, service charge, discounts, voided lines): from the orders that closed in the shift;
    • payments, refunds and cash rounding: from those taken in the shift.

    An order opened in one shift and paid in the next is reported once, in the second.

  • The report is computed, never stored. shiftReport() in packages/core rebuilds it from the events, so a reprint, the hub and the back office all get the same numbers.

  • Expected cash is the opening float plus cash payments (after rounding and change), minus cash refunds, plus cash in, minus cash out. The variance is counted minus expected.

  • Settings (ADR 0014):

    • each device can require an open shift to take payments (on by default, so every sale lands in a Z-report; order-taking phones can turn it off);
    • each outlet can choose a blind close (off by default), which hides the expected cash while counting and on X-reports, until the Z-report prints.
  • Payments taken with shifts switched off belong to no shift. They still count in daily sales reports (M7), but in no Z-report.
  • Open orders do not block closing a shift; the close screen warns about them, and they count in whichever shift they are paid in.
  • shiftId was added to three existing payloads as an optional field without a schema version bump, which was safe before any release. Later payload changes need a new schema version.
  • The event store copies payload.shiftId into an indexed column, so a report reads one shift’s events without scanning the log.

For AI tools: llms.txt ·llms-full.txtopenkedai.com