feat(carwash): Car Wash v1 + per-till shifts + site-level pay-at + till access by module permission
Car Wash — the pilot venue module (wiki/decisions/venue-modules.md): - Master data (categories × services price matrix) at /setup/carwash; the desk at /wash (ticket lookup → order; open queue oldest-first: Done / Paid cash / Paid card / Void; Finished list). Orders freeze names + price; their life is signed (carwash_order, carwash_payment). Migration 0027. - Where money is taken is a SITE setting (carwash_config.pay_at, migration 0028, signed config_change on a flip) — no per-order radio; a stale client is refused (409). - Core seams: PayStation charge providers (a booth-paid wash rides the parking payment as chargeLines) + applyValidation() shared with the merchant route. A bay-paid, done wash signs the $0 parking payment so the exit reader releases the car. - "Parking discount" modes for the wash: free while the wash runs (+ tolerance) and wash price off the fee (floored at 0), resolved at done and anchored at the order's intake (the entry-anchored version comped a 74-day stay); typed-amount and percent hidden for the wash. Long durations render y/d/h/m. Tills — a shift belongs to a till, not the site (wiki/concepts/shift.md §Tills): - TillId booth|carwash; every money event names its till (absent = booth, so the chain re-folds identically). ShiftService is per till: single-open, folds, X/Z-reports, vouchers, carry-forward. A bay payment needs the carwash shift. - Working a till needs that till's module permission (manifest tillPermission; 403 till_forbidden); /api/shift/tills lists only the role's tills. - Web: ShiftButton per till (header = booth, wash desk = carwash); shift hub lists every open shift with till badges + filter; drawer hub switches tills. Modules: landing per module (index route resolves booth → module landing → shifts → profile); guards bounce to "/", /booth needs session:read. Tests: carwash e2e suite (settings, intake, booth/bay paths, modes, void, gate, pay-at policy, till permissions), 6 per-till shift tests; suite green (1 pre-existing flaky backup test under the parallel run). Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
+39
-1
@@ -2,7 +2,7 @@
|
||||
type: concept
|
||||
tags: [parking, domain, business, shifts, anti-fraud]
|
||||
sources: []
|
||||
updated: 2026-07-05
|
||||
updated: 2026-09-05
|
||||
status: open
|
||||
---
|
||||
|
||||
@@ -23,6 +23,12 @@ don't force one model across both.
|
||||
|
||||
## Site-wide single-open + the booth gate (decided + built 2026-06-18)
|
||||
|
||||
> **Superseded 2026-09-05 — now PER TILL (built).** With money-taking venue modules (Car Wash
|
||||
> at the bay), "site-wide" became **per till**: one open shift and one drawer per till
|
||||
> (`booth`, `carwash`, …), each with its own operator, float, vouchers and Z-report; every
|
||||
> money event names its till. See §"Tills" below and [[venue-modules]] §"Tills". Everything
|
||||
> in this section stays true *within* a till.
|
||||
|
||||
A shift is a **site-wide accountability period**: at most **one shift may be open at a time** across
|
||||
the whole appliance. This is what makes a taking unambiguously attributable — every payment/exit
|
||||
falls inside exactly one operator's window. Consequences:
|
||||
@@ -266,6 +272,38 @@ read a slow open as a dead click. Every shift open/close button (header, /shifts
|
||||
"open shift now", the end-shift confirm) now pairs the busy label with an animated spinner
|
||||
(`ui/Spinner.tsx`, reusable) and dims while disabled.
|
||||
|
||||
## Tills — one shift and one drawer per money-taking desk (built 2026-09-05)
|
||||
|
||||
A **till** is a physical cash drawer with its own accountability. The booth is the till that
|
||||
always existed; a venue module that takes money at its own desk declares its own till in its
|
||||
manifest (Car Wash → `carwash`; a future Bar → `bar`). Rules:
|
||||
|
||||
- A shift is **opened on a till**. At most one shift open per till; tills are independent (the
|
||||
booth and the wash desk run side by side, by different — or the same — operators).
|
||||
- **Every money event names its till** (`payload.till`): parking `payment` and the
|
||||
subscription sale = `booth` (a wash paid at the booth rides the parking payment as
|
||||
`chargeLines`, so it is booth money too); `carwash_payment` at the bay = `carwash`;
|
||||
`cash_in`/`cash_out` carry the drawer they moved. `shift_open`/`shift_z_report` carry theirs.
|
||||
- **Absent `till` = booth.** Every event before tills existed is booth money, so the chain
|
||||
re-folds identically and old Z-reports read as booth shifts. `tillOf()` in `@parking/shared`
|
||||
is the one place this rule lives.
|
||||
- The drawer fold, the X/Z-report window (payments **and** vouchers) and carry-forward all
|
||||
filter by till: the wash operator's expected drawer is *their* float + *their* bay cash +
|
||||
*their* vouchers, and the booth's never includes bay money. The counted-vs-expected moment
|
||||
therefore sits with whoever holds the cash — which is the whole point (see §below).
|
||||
- "Take money at the bay" requires the **carwash** shift, not the booth's; the wash desk
|
||||
carries its own shift control. The header button stays the booth's. The shift hub lists
|
||||
every open shift with a till badge; the drawer hub switches tills.
|
||||
- **Working a till needs that till's module permission** (added 2026-09-05 after the user
|
||||
found a wash user could open the *booth's* shift): the manifest names it
|
||||
(`tillPermission` — booth: `session:read`, carwash: `carwash:read`), `tillsFor()` in
|
||||
`@parking/shared` resolves a role's tills, the shift/drawer routes refuse the rest with
|
||||
`403 till_forbidden`, and `/api/shift/tills` + `current.tills` return only the role's
|
||||
tills — so the header button, the hub's start buttons and the drawer switch never offer a
|
||||
till the server would refuse. `shift:create` alone opens nothing.
|
||||
- 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.
|
||||
|
||||
## Where the fraud control actually lives
|
||||
|
||||
Deliberately **not** in a shift-close ceremony. Because every payment is a **signed event in the
|
||||
|
||||
Reference in New Issue
Block a user