--- type: concept tags: [parking, domain, business, shifts, anti-fraud] sources: [] updated: 2026-06-15 status: open --- # Shift (manned mode) & the Z-Report A **shift** is one operator's accountability period at a manned booth: from the moment they take over to the moment they hand over, however long that is. At the end, the system signs and **prints a Z-report** — the cash and POS totals taken during the shift. (Decisions 2026-06-15.) ## Shifts exist ONLY in manned mode A shift is fundamentally a **human accountability boundary** — "this person was responsible for the takings from here to here." In the [[autonomous-direction|fully-automated / unmanned]] system there is **no operator and no shift**; what replaces it is the pay station's **cash-collection cycle** (who emptied the vault, when, how much vs. what the signed log expected) plus ongoing [[reconciliation]] — a separate concept, not a shift. So shifts are scoped to manned operation; don't force one model across both. ## A shift is NOT time-based It is delimited by **explicit operator action**, never by a clock: - Booth reality: relief comes late, doesn't show, or one operator is **forced to work two shifts in a row**. A fixed 8h boundary (or an 8h token expiry) would be wrong — it could strand an active operator. So the [[local-jwt-auth|login token has no time expiry]] (valid until logout). - **Start Shift / End Shift are explicit, and independent of login.** One login can span many shifts; a back-to-back double is simply *End Shift → Start Shift again*, no re-login. The operator (the same person or the next) marks the boundary. ``` login ——————————————————————————————————————————————→ (until logout) [Start shift] … takings … [End shift→sign+print Z] [Start shift] … [End shift] … ``` ## What End Shift does 1. Determine the shift's payment set: the signed `payment` events ([[parking-session]], [[append-only-event-chain]]) between this shift's start mark and now. 2. Sum by **tender**: `cashTotal`, and `cardTotal` from the POS/terminal **if a POS is configured** (the card line is omitted when there's no terminal). 3. Append a signed **`shift_z_report`** event (type already in `packages/shared`): `{ operator, startedAt, endedAt, cashTotal, cardTotal?, paymentCount, eventRange, prevZHash }` — chained to the prior Z so a missing/out-of-order Z-report is itself visible. 4. **Print the Z-report** (cash total, POS total if any, counts, shift window, operator) on the booth printer. That's the whole human-side requirement: **print the cash and the POS (if any).** No blind count, no variance gate, no manager override. ### As-built (2026-06-16) - A shift is **two signed ledger events**, no mutable table (decision): `shift_open` (new event type) at start, `shift_z_report` at close. The operator is the **logged-in user**, carried in the event `identity`; a shift is **open** iff that operator's most recent shift event is a `shift_open`. `ShiftService` (`apps/server/src/shift-service.ts`). - **Close** sums `payment` events in `[startedAt, endedAt]` by tender (cash vs. card, by **payment time**), appends the signed `shift_z_report` (totals + counts + window), then **prints** via the new generic `PrinterDevice.printReport(title, lines)` (Rongta ESC/POS text) to a booth-receipt printer. Printing is best-effort — a failed print does **not** undo the signed close (the event is the record; `printed:false` is returned). - **Routes** (`routes/shift.ts`, cashier/operator/admin): `GET /api/shift/current`, `POST /api/shift/open` (409 if already open), `POST /api/shift/close` (409 if none open). **UI** `ShiftControl` in the app shell (non-readonly): Start/End + the Z-report totals. - Verified: open → double-open 409 → payments (cash+card, one dated outside the window excluded) → close totals correct + signed + printed → close-again 409 → re-open works; readonly 403; verifyChain ok. ## Where the fraud control actually lives Deliberately **not** in a shift-close ceremony. Because every payment is a **signed event in the append-only chain**, the printed cash figure *is* the system's tamper-evident truth. A manager reconciles the signed Z-report against the actual drawer and the bank/POS batch **later** — that's [[reconciliation]], the real control (deferred). The tradeoff vs. a heavier control is purely *when* a skim is caught (after the fact, by a human), not *whether*. > **Optional enhancement (not building now): blind cash count.** Have the operator enter the > counted cash *before* the system reveals the expected figure, and record the variance into the > `shift_z_report`. Blindness removes the operator's ability to back-fill their declaration to match > expectation, catching a skim **at close** rather than later. Explicitly out of scope per > 2026-06-15; documented as a clean add-on if ever wanted. ## Open - **Shift ↔ session boundary:** a vehicle may enter under one shift and pay under another — the Z-report sums by **payment time** (when cash/card was taken), which is the operator who handled the money. Confirm that's the intended accountability (vs. by entry). - **Mid-shift report / X-report** (read-only "so far" total without closing) — add if booths want it; the sum is the same projection. - **Multiple lanes/booths** — whether a shift is per-operator, per-booth, or per-site (relates to [[open-questions]] #1 lane topology).