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:
+66
@@ -2937,3 +2937,69 @@ activation with dependency rules and one signed config_change per module that ac
|
||||
and router.tsx spreads WEB_MODULES into nav + route tree, SiteSettings gets a Modules panel and
|
||||
hides the validation section when the module is off; MODULES_ENTITLED set explicitly in both
|
||||
booth stacks. 7 new server tests, suite 329/329, web build clean. Car Wash next.
|
||||
|
||||
## [2026-09-05] feat | Car Wash v1 built as the pilot venue module — one manifest, two folders, two core seams
|
||||
|
||||
Delivered the pilot on [[venue-modules]]: carwash_{categories,services,prices,orders} (migration
|
||||
0027), ledger types carwash_order (created/done/void, names + price frozen) and carwash_payment
|
||||
(bay money), a server module (settings, oldest-first queue, intake against an open ticket, done →
|
||||
applies the "carwash" validation program via the extracted applyValidation(), bay payment →
|
||||
signs carwash_payment and, if the sponsorship made the session zero-due, the $0 parking payment
|
||||
the exit reader needs; void takes back a live sponsorship) and a web module (/wash desk,
|
||||
/setup/carwash master data + sponsorship editor). Two deliberate core seams: PayStation charge
|
||||
providers (a wash paid at the booth rides the parking payment as chargeLines; BoothPayModal shows
|
||||
them) and applyValidation() shared with the merchant route; shift money folds include bay
|
||||
payments. Registry proof: the module needed exactly one manifest entry + one line in each
|
||||
registry + its folders. 7 new tests, suite 337/337; full bay flow verified live in the browser.
|
||||
Booth stacks not yet entitled to carwash. Next increment: vision category flag + bay camera.
|
||||
|
||||
## [2026-09-05] query | Car Wash review pass: discount modes, price matrix, 74-day comp bug, finished list — and the tills requirement
|
||||
|
||||
Hands-on review of Car Wash v1 with the user, recorded on [[venue-modules]] §"Review log":
|
||||
renamed the discount section; added two wash-only discount modes (free during the wash +
|
||||
tolerance; wash price off the fee, floored at 0) resolved at done via applyValidation() and
|
||||
refused at merchant scan; hid typed-amount and percent from the wash editor (typed amount =
|
||||
the operator picks the money → highest fraud exposure; kept for the Bar); fixed the
|
||||
price-matrix cells for unsaved rows (two-request save); found and fixed a real money bug —
|
||||
"free until done" was anchored at entry and would have comped a 74-day stay for one wash
|
||||
(ticket 92498375903) — now anchored at the order's intake; long durations render y/d/h/m;
|
||||
added a Finished list to the desk. Raised a structural requirement: shifts must become
|
||||
per-till (booth / carwash) — a bay payment currently needs the booth shift and folds into
|
||||
the booth drawer, which breaks both operators' Z-reports; design recorded, awaiting a go
|
||||
([[shift]] carries a forward pointer).
|
||||
|
||||
## [2026-09-05] ingest | Tills built: one shift + one drawer per money-taking desk
|
||||
|
||||
The user confirmed the tills design ("go ahead and start building it"). Built in the core:
|
||||
a shift is opened on a till (`booth` | `carwash`), every money event names its till
|
||||
(`payload.till`, absent = booth so the chain re-folds identically), the ShiftService folds,
|
||||
X/Z-reports, vouchers and carry-forward are per till, single-open is per till, and a bay
|
||||
payment now requires the **carwash** shift. Web: the shift button became a per-till
|
||||
component (header = booth, wash desk = carwash, with "Wash drawer now" and pay buttons
|
||||
gated on my wash shift); the shift hub lists every open shift with till badges + a till
|
||||
filter; the drawer hub switches tills. 6 new shift tests + the bay test now proves the booth
|
||||
shift does not cover the bay; verified live with the booth held by another operator. Recorded
|
||||
on [[shift]] §"Tills" and [[venue-modules]] §"Tills → As-built". Follow-ups: per-till
|
||||
activity log, wash bucket on the booth Z, wash-desk printer role.
|
||||
|
||||
## [2026-09-05] ingest | Where wash money is taken became a site setting (Setup → Car wash)
|
||||
|
||||
User: the booth|bay choice belongs in `/setup/carwash` ("Pagesa: në kabinë / në lavazh"), and
|
||||
the per-order radio goes away from `/wash`. Built: `carwash_config` singleton (migration 0028,
|
||||
default booth), `payAt` on the settings view/body, signed `config_change carwash.payAt` on a
|
||||
flip, orders freeze the policy in force, `409 pay_at_policy` for a stale client; Setup radio;
|
||||
desk shows the policy read-only. Tests: 1 new (default, persist, sign, freeze, refuse).
|
||||
Recorded on [[venue-modules]] §"v1 answers" item 2.
|
||||
|
||||
## [2026-09-05] ingest | Till access by module permission; landing per module
|
||||
|
||||
The user found their wash user could open the booth's shift (any `shift:create` could open any
|
||||
till) and asked for the wash interface to be filtered off booth screens. Built: manifest
|
||||
`tillPermission` + `tillsFor()`; server `accessibleTillsFor()` guards shift open/close/state
|
||||
and cash movements (`403 till_forbidden`), `/api/shift/tills` returns only the role's tills;
|
||||
header shift button needs `session:read`; module `landing` replaces the hard-coded merchant
|
||||
landing, all guards bounce to `/`, `/booth` needs `session:read`. Diagnosed the dev
|
||||
`Lavazhier` role: it holds booth permissions (`session:read`, `payment:create`,
|
||||
`session:create`) and lacks `carwash:create/update` — a role problem, not a code one. The
|
||||
WebSocket stays `report:read`-only by design; the desk polls. Recorded on [[shift]] §Tills
|
||||
and [[venue-modules]] §Tills → As-built.
|
||||
|
||||
Reference in New Issue
Block a user