# 0017. Shifts and Z-reports

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

## Context

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.

## Decision

- **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.

## Consequences

- 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.
