From 28bd83869671952ac795f94f6949b8d5a7a56679 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Mon, 13 Jul 2026 19:50:09 +0200 Subject: [PATCH] docs(wiki): merchant validations settled + as-built; scan input decided (camera paths postponed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validation-discounts: driving cases → the settled validation-only model (all money/paper at the booth) → setup UX/storage/RBAC → full as-built record. DECIDED: merchant stations scan with a USB/HID barcode scanner on the web/desktop app (hand-keying + Luhn as fallback); POSTPONED with analysis: web getUserMedia scanning (secure-context TLS prerequisite on the LAN + Code128-via-camera weakness → QR-on-ticket first) and a Tauri v2 Android merchant app (native ML Kit scanning; Android build/sideload overhead + configurable-server-URL prerequisite). Also: wsl-dev-networking gains the mirrored-mode gotcha where a Windows-side listener makes a port EADDRINUSE inside WSL while invisible to ss — Vite auto-increments and tauri dev's fixed devUrl waits on the wrong port. Claude-Session: https://claude.ai/code/session_01YYkpEsLmoQPaize5ec3oUm --- wiki/concepts/validation-discounts.md | 129 +++++++++++++++++++++++++- wiki/concepts/wsl-dev-networking.md | 11 +++ wiki/index.md | 2 +- wiki/log.md | 59 ++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) diff --git a/wiki/concepts/validation-discounts.md b/wiki/concepts/validation-discounts.md index f9fb9d3..a39396b 100644 --- a/wiki/concepts/validation-discounts.md +++ b/wiki/concepts/validation-discounts.md @@ -2,7 +2,7 @@ type: concept tags: [parking, domain, business, pricing, revenue] sources: [] -updated: 2026-06-15 +updated: 2026-07-13 status: open --- @@ -11,6 +11,133 @@ status: open A merchant (shop, hotel, clinic) **validates** a customer's parking so they pay less or nothing — a common revenue/retention feature that modifies what a [[parking-session]] owes. +## Driving cases (owner requirements, 2026-07-13) + +The feature moved from "industry gap" to **asked-for**: the park may contain an in-park +**car-wash (al. "lavazh")** and/or a **bar**, and the owner wants their customers discharged +(fully or partly) for the parking stay: + +- **Car-wash**: parking free entirely, **or** free for an owner-set duration (30 min / 1 h / 2 h …) + after which the stay prices like any transient → `comp` or `time-credit`. +- **Bar**: subtract the bar consumption from the parking fee (consumed 300 ALL, park fee 500 ALL → + pay 200 ALL) → `fixed` with a **per-use variable amount**; or parking free for bar customers → `comp`. +- These must be **admin-composable at runtime like tariffs/subscription plans** — the owner + defines the programs and their parameters; nothing hard-coded. + +**Refined the same day (settled): the merchant is a VALIDATION-ONLY system user; ALL money and +paper stay at the booth.** Ownership is immaterial and the [[validation-sponsorship]] +sponsor/settlement layer is **not needed** for this. The model: + +- A **merchant user** (the "bar user", "lavazh user") logs into the system on their own device and + **scans the customer's ticket** there — the scan-and-apply *is* the validation, a signed event + attributed to that user (accountability sits with the merchant, not the booth operator). That is + the merchant's ENTIRE surface: no payment collection, no printer, no shift. +- **Every car still checks in at the booth to settle** — even a fully-comped one. The booth quote + applies the session's validation events (`gross − discounts`, floor 0); the operator collects the + **net** (possibly 0 — a zero-amount settlement is still a signed `payment` event so grace/exit + work unchanged) and **prints the detailed receipt there** (gross fee, each validation line, net + paid). +- Exit is the unchanged [[booth-exit-flow]] (immediate exit or voucher self-exit at the reader). + +This DISSOLVES the two consequences flagged by the earlier merchant-collects variant (rejected +2026-07-13, same conversation): the [[shift]] site-wide single-open invariant and single till stay +as built (Z/X-reports just gain gross/discount/net lines so cash reconciles to net), and the exit +reader needs no live due=0 branch (the booth settlement covers the zero-due case; time-credit is +priced at booth check-in, inside the normal walk-back-grace flow). + +## Settled design (2026-07-13) — setup UX, storage, RBAC + +- **Setup lives on `/setup/site`** (gated by the page's existing `site:update`): the left card + gains **Bar** and **Lavazh** checkboxes; the empty right column renders the enabled station's + config panel (tabs when both). Panel per station: **mode** (comp / time-credit N-min / fixed + amount-typed-at-scan with a max cap / percent), **caps** (max per validation, max per day, + one-per-session default), **receipt label**, **bound users**. +- **Fixed UI, generic storage**: a `validation_programs` table (+ user binding) where Bar and + Lavazh are two **well-known rows** created on first enable — a third merchant later is a data + row, not a migration (honours the "composable like tariffs" requirement). Config is plainly + **mutable, no versioning**: the applied validation is a signed ledger event carrying the + RESOLVED values (minutes/amountMinor + programId), so reproducibility never depends on the row. + Enabling/saving signs a `config_change` ([[entry-presence-bypass]] precedent). +- **RBAC**: new `validation` resource in the code-defined grid — `validation:create` (apply; the + merchant's only permission) + `validation:read` (reports/history). Guard = permission **AND** + station binding (data), so a bar user can never apply the lavazh program. Merchant users land on + a new **`/validate`** screen (scan → session → apply); the permission-driven nav shows them + nothing else. Program composition needs no new permission (`site:update`). +- **Mistake handling**: a merchant may **void their own validation while unused** (before it + entered a payment) — a signed void event, never a delete. Booth/admin can void via the normal + event-void path. +- Open (non-blocking): per-customer mode choice (v1 = one mode per station); merchant scan + hardware — lean: also print a **QR** of the ticket id so any phone camera works + ([[ticket-encoding]]). + +## As-built (2026-07-13) + +- **Shared (`@parking/shared`)**: `validation` resource (`validation:create`/`read`) in the + permission grid; `ValidationMode`/`ValidationProgram`/`SessionValidation`/`ValidationLine`; + `priceSession(…, validations[])` folds the discounts in a **canonical order** — timeCredit + (shifts the billed period's start forward, so grace/steps/windowed cards price the remainder + correctly) → percent (of the remainder) → fixed (clamped) → comp — net floors at 0 and + **Σ lines ≡ gross − net** by construction. Unit-tested (incl. overstay + settled cases). +- **Ledger**: new `validation` event type — payload carries the **resolved** values + (`programId`, `programLabel`, `mode`, `minutes`/`amountMinor`/`percent`) + `operator` (the + merchant username); `refId` set = a VOID of the referenced validation (append-only, mirrors + `cash_review`). The settling `payment` records `grossMinor`/`discountMinor`/`validationIds` + (**consumption** — an overstay's fresh period never re-applies them) + `validationLines` + (receipt reproducibility). +- **DB**: `validation_programs` + `validation_program_users` (migration `0024`; both in + reset-db's `config` category). Mutable master data, soft-deletable. +- **Server**: `routes/validations.ts` — programs GET/PUT (`site:read`/`site:update`, signed + `config_change` on real change only), `/mine`, `/session/:identity` (deliberately no money + data), `/apply` (guards in order: program live+active → user **bound** → open **transient** → + no live duplicate of the program → `maxPerDay` → fixed-amount bounds), `/void` (own + + unconsumed only). `PayStation.quote/lookup/pay` fold `liveValidations` (applied − voided − + consumed); `activeSessions` amounts are net automatically. Receipt (`renderReceipt`) prints + gross (`Tarifa`) + one line per discount; the big amount is the NET. Z/X-report gained + `discountTotalMinor` (leakage; takings stay net) — printed as `Zbritje (validime)` only when + non-zero, so old slips stay byte-identical. +- **Web**: `/setup/site` is two-column — Bar/Lavazh checkboxes on the left card (a flip persists + `active` at once = signed config change), `ValidationSetup.tsx` panel on the right (tabs when + both; mode/params/caps/receipt-label/bound-users). `/validate` (`ValidateScreen.tsx`) is the + merchant's whole surface (scan/key → apply → void own unused), mobile-friendly, autofocused + input works with HID scanners; merchant-only users (no `session:read`) land there on login and + the permission-gated nav shows them nothing else. Booth pay modal shows gross → lines → net; + the zero-net comp settles through the normal pay path (grace starts, voucher/exit unchanged). + Feed label `VALIDIM`/`VALIDATION`. RolesManager picks the new resource up generically. +- **Verified**: 8 route-level integration tests (guards, signed events, money cycle, void locks, + per-day cap) + the shared fold suite; whole-workspace build/typecheck/test green; migration + applied to the dev DB. +- **Remaining polish (not blocking)**: show `discountTotalMinor` in the X-report/close-modal/ + shift-history UI (it's already in the signed payload + printed Z); a validations/leakage + **report** (per program/user/day) under [[reporting-analytics]]. + +## Merchant scan input — DECIDED 2026-07-13: barcode scanner on the web/desktop app; camera paths POSTPONED + +**v1 (in force):** the merchant scans with a **USB/HID barcode scanner** into the `/validate` +screen on the web (or desktop) app — the scanner types the 11-digit id + Enter into the +autofocused input, exactly like the booth. Hand-keying is the zero-hardware fallback; the +[[ticket-encoding|Luhn check digit]] catches typos. The park site is expected to equip the +bar/lavazh station accordingly — no phone-camera path for now. + +**Postponed (evaluated 2026-07-13, both viable, deliberately deferred):** + +1. **Web camera scanning** — `BarcodeDetector` (Chromium/Android native) + the `barcode-detector` + polyfill on **zxing-wasm** (Apache/MIT — license-clean, bundles offline). Two prerequisites + killed it for now: (a) `getUserMedia` needs a **secure context** — a merchant phone on + `http://` gets NO camera, so the appliance needs a TLS story (realistically a + self-signed CA minted on the booth + one-time cert install per device — fold into the + [[booth-deploy-networking|reverse-proxy]] plan); (b) Code128 via phone camera on thermal + paper decodes poorly — would want the **QR-of-ticket-id** addition first (the ESC/POS driver + already has `qrCode()`; `renderTicket` is a one-line change — still a good idea whenever any + camera path revives). +2. **Tauri Android merchant app** (a SECOND small Tauri target, e.g. `apps/validator` — NOT an + extension of [[desktop-shell-tauri|apps/desktop]], which is a booth kiosk hardwired to + localhost:3000): Tauri v2 mobile + the official `barcode-scanner` plugin (ML Kit — reads + Code128 well natively, and the tauri:// origin is secure so the TLS problem vanishes). + Costs that drove the postponement: Android SDK/NDK + Rust-target build infra (+CI), APK + sideload distribution/updates to merchant devices, effectively Android-only (iOS needs a + paid signing account), and it needs the configurable-server-URL work the desktop shell also + wants. Revisit if the owner issues dedicated Android tablets to merchants. + ## Model: a discount is a signed event, applied at fee time A validation is **not** an edit to the session or a mutable "discount applied" flag — same reason diff --git a/wiki/concepts/wsl-dev-networking.md b/wiki/concepts/wsl-dev-networking.md index 0f27b17..4b8ae79 100644 --- a/wiki/concepts/wsl-dev-networking.md +++ b/wiki/concepts/wsl-dev-networking.md @@ -55,6 +55,17 @@ Mirrored networking is necessary but **not sufficient** — these still bit us: - **`localhost` → IPv6 first.** `localhost` resolves to `::1`, but the backend binds IPv4 (`127.0.0.1`). Node's Vite proxy can stall on the v6 attempt before falling back — point the proxy at `127.0.0.1` explicitly. (See [[local-dev-workflow]].) +- **Windows-side listeners collide with WSL binds — INVISIBLY (2026-07-13).** Under mirrored + mode, a process listening on the WINDOWS side makes the same port `EADDRINUSE` inside WSL, + but it never appears in Linux `ss`/`lsof` — the port looks free yet won't bind. Bit us as + "tauri dev: Could not connect to http://localhost:5173 after 180s": a DIFFERENT React app's + dev server running on the Windows side held `::1:5173`, so the WSL Vite silently + auto-incremented to 5174 while Tauri's `devUrl` is the FIXED string `http://localhost:5173` + in `tauri.conf.json` (it cannot follow the auto-increment). Diagnose from WSL with + `powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort 5173 -State Listen"` + (then `Get-Process -Id `); kill with `taskkill.exe /PID /F`. Guard: + `strictPort: true` in the web `vite.config` so the mismatch fails in a second with a clear + error instead of a 3-minute hang on the wrong port. ## Multi-subnet source-address trap (the "ARP works but ping/TCP dies" bug) diff --git a/wiki/index.md b/wiki/index.md index 354ac1e..ccbcdd2 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -99,7 +99,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records. - [[capacity-occupancy]] — live count = open sessions; refuse entry + FULL sign when full (soft policy); exit never blocked. - [[site-metadata]] — optional park identity (name, operator, VAT, address, contact) in site_config; feeds the ticket header. - [[valet-overcapacity]] — "full" is soft: operator may valet-accept over capacity (keys handed over, custody). Manned, deferred. -- [[validation-discounts]] — merchant validates a ticket → signed discount event applied at fee time. +- [[validation-discounts]] — BUILT (2026-07-13): in-park merchant (bar/lavazh) users scan-and-validate on their device (signed event, program↔user binding); booth settles NET + prints gross/discount/net; comp/time-credit/fixed/percent, caps, /setup/site panel, /validate screen. - [[validation-sponsorship]] — design: sponsor accounts + postpaid B2B (customers park free, business billed monthly); not a permit. - [[reporting-analytics]] — revenue/occupancy/stay reports + plate-search, all projections over the signed log. - [[clock-integrity]] — fees depend on the host clock; detect/flag backdating on an offline box. diff --git a/wiki/log.md b/wiki/log.md index d16275e..3aab68d 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -2560,3 +2560,62 @@ privilege + a signed ledger event with actor+reason, with the booth as enforcer fail-open that can't depend on the cloud. Four open questions parked (real-time definition, where reports are computed, hosting/licensing, custodianship-as-liability). Cross-linked; index count 7→8 decisions. + +## [2026-07-13] update | Owner requirement — in-park merchant validations (car-wash "lavazh", bar) + +The [[validation-discounts]] feature is now asked-for, not just an industry-survey gap: the park +may host an in-park car-wash and/or bar whose customers the owner wants discharged for the stay — +full comp, free-first-N-minutes (`time-credit`), or consumption-offset (`fixed`, variable amount: +300 ALL consumed vs 500 ALL fee → pay 200). Must be admin-composable at runtime like +tariffs/subscription plans. Driving-cases section added to [[validation-discounts]]. Open: merchant +ownership (owner-run → pure discount; tenant → [[validation-sponsorship]] settlement), who applies +(operator vs merchant code/portal), stacking rules, caps. + +## [2026-07-13] update | Merchant validations refined — merchant STATIONS (users), not sponsors + +Second pass on the [[validation-discounts]] requirement: ownership immaterial, sponsor layer +dropped. Merchant = a system user on their own device who scans the ticket to validate (signed, +attributed); admin checkbox per station = may collect parking payments (then shift + till + +Z-report apply to them like the booth); paid/zero-due tickets self-exit at the reader. +Consequences: per-station shifts/drawers (breaks the site-wide single-open invariant), exit-reader +live due=0 branch. Details on [[validation-discounts]]. + +## [2026-07-13] update | Merchant validations settled — validation-only merchants, all money at the booth + +Third pass, settled: the merchant-collects-payments variant is REJECTED. Merchant users only scan ++ validate (signed, attributed); every car checks in at the booth to settle (net may be 0 — still +a signed payment) and gets the detailed gross/discount/net receipt there. Per-station +shifts/drawers and the exit-reader due=0 branch are no longer needed — shift/drawer/exit flows +stay as built; Z/X-reports gain discount lines. Build surface: validation_programs master data, +signed validation event, priceSession validations[] extension, merchant scan page, booth +quote/receipt/Z-report lines. Details on [[validation-discounts]]. + +## [2026-07-13] decision | Merchant validations — design SETTLED, build started + +Setup UX on /setup/site (Bar/Lavazh checkboxes → right-column config panel, tabs when both); +fixed UI over generic storage (validation_programs + user binding, well-known bar/lavazh rows, +mutable config — the signed validation event carries resolved values); RBAC = new `validation` +resource (create/read), guard = permission AND station binding; merchant-only users land on +/validate; merchants may void their own unused validation. See [[validation-discounts]]. + +## [2026-07-13] update | Merchant validations BUILT end-to-end (bar / lavazh) + +Shipped the settled design: `validation` permission + ledger event (resolved values, refId-void), +priceSession validations[] canonical fold (timeCredit→percent→fixed→comp, Σ lines ≡ gross−net), +validation_programs(+users) tables (migration 0024, reset-db config category), routes/validations.ts +(programs PUT signs config_change; apply guards: binding → open transient → no dup → maxPerDay → +amount cap; void own-unused-only), PayStation quote/pay/lookup net folding + payment consumption +(grossMinor/discountMinor/validationIds/validationLines), receipt gross+discount lines, Z/X-report +discountTotalMinor ("Zbritje (validime)", printed only when >0), /setup/site two-column Bar/Lavazh +checkboxes + config panel (tabs), /validate merchant screen (merchant-only users land there), +booth-modal gross→lines→net, feed label VALIDIM. 8 new route integration tests + shared fold suite; +workspace build/typecheck/test green. As-built + remaining polish on [[validation-discounts]]. + +## [2026-07-13] decision | Merchant scan input: HID barcode scanner on web/desktop; camera paths postponed + +The bar/lavazh stations use a USB/HID scanner (or hand-keying + Luhn) into /validate on the +web/desktop app. Two evaluated camera alternatives deliberately POSTPONED: web getUserMedia +scanning (blocked on secure-context TLS for LAN phones + weak Code128-via-camera — would want +QR-on-ticket first) and a Tauri v2 Android merchant app (native ML Kit scanning via the official +barcode-scanner plugin; deferred over Android build/distribution overhead + the +configurable-server-URL prerequisite). Full analysis on [[validation-discounts]].