feat(tills): per-till activity log, wash bucket on the booth Z-report, wash-desk printer role

Closes the three known follow-ups of the Tills decision (venue-modules.md):

- Activity log per till: `tillOfEvent(type, payload)` in @parking/shared (money events
  by payload till, other events by their owning module's till, everything else booth),
  applied by `/api/events?till=` in SQL and passed by the hub log, the Drawer "today"
  panel and the booth feed (history + live pushes). The events route admits a role that
  holds a module feed permission without event:read and returns only that module's
  event types — the live-socket rule.
- Booth Z-report: `chargesByModuleMinor` sums the chargeLines on the till's payments by
  module; the ticket bucket excludes them (Bileta = parking only); printed
  "Lavazh (në biletë)" only when any was taken. The wash till's slip prints "Lavazh:".
- Printer role `wash-desk`: the wash till's Z-report and vouchers print there, falling
  back to the booth printer; nothing falls back to the desk. `printerRoleOf()` is the
  one reading of the role field (the entry/booth loaders treated any non-booth role as
  an entry dispenser). Footer label "at wash desk".

Also: `GET /api/carwash/settings` opens to carwash:read OR site:read (new
requireAnyPermission) — the Wash operator job could not load the desk's category and
service pickers. Tests for all four; wiki (shift, printer-roles-failover, venue-modules,
log) updated.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-06 12:37:26 +02:00
parent ea304bbfd1
commit e14e31a840
27 changed files with 414 additions and 91 deletions
+14 -2
View File
@@ -17,8 +17,10 @@ Each printer instance (a `devices` row, category `printer`) declares a **role**
config:
- **`entry-dispenser`** — outside, at the lane. Prints the entry ticket the driver takes.
- **`booth-receipt`** — inside the booth. Prints receipts at exit/payment, AND serves as the
**backup** for entry tickets.
- **`booth-receipt`** — inside the booth. Prints receipts at exit/payment, the booth till's
Z-reports and vouchers, AND serves as the **backup** for entry tickets (and for wash slips).
- **`wash-desk`** — at the Car Wash desk (added 2026-09-06). Prints the wash till's slips:
its Z-report and drawer vouchers (see [[shift]] §Tills). Nothing else ever prints here.
It also declares a **`failoverRank`** (higher = preferred within a role) to order multiple
printers of the same role deterministically (ties broken by id).
@@ -33,6 +35,16 @@ The reverse is **deliberately not** done: a **receipt** never prints on the outs
Receipts are a booth-only job; an entry dispenser falling back to print receipts makes no
physical sense.
For a **wash slip** (`wantRole = wash-desk`): the desk printers first, then the **booth
printer** — a site that has not bought a desk printer keeps printing the wash till's Z-report
and vouchers in the booth, exactly as it did before the role existed. Nothing falls back *to*
the wash desk: a booth receipt or an entry ticket never prints there. `ShiftService` resolves
the role from the till (`TILL_PRINTER_ROLE`: booth → `booth-receipt`, carwash → `wash-desk`)
and keeps one legacy fallback — a booth with a single printer that carries no booth role still
prints its slips on it. `printerRoleOf(config)` is the one reading of the saved `role` field,
so every loader (entry flow, booth receipts, shift slips) agrees on what a printer is; the
device footer shows a desk printer as "at wash desk".
## Where the logic lives
- The driver (`rongta`) is **role-agnostic** — role/rank are just config; the transport doesn't
+19 -2
View File
@@ -305,8 +305,25 @@ manifest (Car Wash → `carwash`; a future Bar → `bar`). Rules:
touch the booth by construction; the header button, the hub's start buttons and the
drawer switch never offer a till the server would refuse. (A first cut that borrowed
`session:read` as "works the booth till" lived for a few hours and is gone.)
- Not done: the per-shift *activity log* is still a time window over the whole chain (money
figures are per till, the event list is not); bay slips print on the booth printer.
- **The activity log is per till too (2026-09-06).** `tillOfEvent(type, payload)` in
`@parking/shared` extends the money rule to every event: a money event names its till, any
other event belongs to the till of the module that owns its type (a `carwash_order` is
wash-desk activity though no money moved), everything else — entries, exits, barrier
commands, pre-till events — is the booth's. `/api/events?till=` applies the same rule in
SQL (so the page limit applies after the filter); the hub's shift log, the Drawer "today"
panel and the booth feed (history and live pushes) pass their till. The events route also
admits a role without `event:read` that holds a module's feed permission, and then returns
only that module's event types — the live-socket rule, so a wash operator's hub shows the
wash shift's log.
- **The booth Z-report breaks module money out (2026-09-06).** `chargesByModuleMinor`
(`{ carwash: <minor> }`, only when any was taken) sums the `chargeLines` on the till's
payments by owning module; the ticket bucket EXCLUDES it, so `Bileta` is parking money only
and ticket + subscriptions + Σcharges = cash + card. Printed as `Lavazh (në biletë): X` on
the booth slip; the wash till's own slip prints its takings under `Lavazh:` (it sells no
tickets or subscriptions). Signed on `shift_z_report`; older reports read back as `{}`.
- **The wash till prints on its own printer (2026-09-06).** Printer role `wash-desk`; the
wash till's Z-report and vouchers go there, falling back to the booth printer — see
[[printer-roles-failover]].
## Where the fraud control actually lives
+6 -5
View File
@@ -522,11 +522,12 @@ control against the unrecorded-wash vector, and it must sit with the person hold
→ Roles); a permission-scoped live feed for module desks (the WS is `report:read` only —
the wash desk polls, 5 s / 15 s).
**Known follow-ups.** A shift's *activity log* (right pane of the hub, Drawer "today") is
still a time window over the whole chain, so a booth shift's log shows wash events in that
window (money figures are per till; the log is not). A separate wash bucket on the booth's
Z-report (booth-paid washes ride `chargeLines`) is still open. Bay slips print on the booth
printer until a wash-desk printer role exists.
**Follow-ups, closed 2026-09-06** (details on [[shift]] §Tills and [[printer-roles-failover]]):
the activity log is per till (`tillOfEvent`, `/api/events?till=`; a feed-permission role
reads its module's events without `event:read`); the booth Z-report carries
`chargesByModuleMinor` (a booth-paid wash is out of the ticket bucket, printed
`Lavazh (në biletë)`); the wash till's slips print on a `wash-desk` printer, falling back
to the booth's.
## Review log — issues and ideas from the first hands-on pass (2026-09-05)
+21
View File
@@ -3049,3 +3049,24 @@ design error: the discount ENGINE (validation program rows + `applyValidation()`
`validation` module is only the merchant's scan screen. Fixed: `dependsOn: ["parking"]`; the
program routes are plain site:read/site:update; the merchant routes (mine/lookup/apply/void)
stay module-gated. Tests updated. Recorded on [[venue-modules]] (v1 answers item 4 + As-built).
## [2026-09-06] ingest | Tills follow-ups closed: per-till activity log, wash bucket on the Z, wash-desk printer
The three "known follow-ups" of the Tills decision are built. (1) `tillOfEvent(type, payload)`
in `@parking/shared` — money events by payload `till`, other events by their owning module's
till, everything else booth — is applied by `/api/events?till=` in SQL and passed by the hub
log, the Drawer "today" panel and the booth feed; the events route now admits module-feed
roles (a wash operator's `carwash:read`) and returns only their module's types, the same rule
the live socket uses. (2) `chargesByModuleMinor` on the shift report/summary/signed payload:
module charges on the till's payments by module; the ticket bucket excludes them; printed
`Lavazh (në biletë)` on the booth slip; the wash till's slip prints `Lavazh:` for its own
takings. (3) Printer role `wash-desk`: the wash till's Z-report and vouchers print there with
failover to the booth printer; `printerRoleOf()` is the one reading of the role field so a
desk printer is never mistaken for an entry dispenser; footer label "at wash desk". Updated
[[shift]] §Tills, [[printer-roles-failover]], [[venue-modules]].
## [2026-09-06] ingest | Wash operator job could not load the desk's price list
User built a role from the "Wash operator" chip (carwash:read/create/update/cash) and the desk's
category/service pickers stayed empty. Cause: `GET /api/carwash/settings` was guarded by
`site:read` only — the price list is Setup's data AND the desk's working data. Fixed with a
new `requireAnyPermission(...)` guard (auth.ts): the read opens to `carwash:read` OR
`site:read`; the write stays `site:update`. Regression test in carwash.test.ts.