fix(subs): price out-of-window charge from minutes actually parked, not a fixed entry stamp

An out-of-window subscriber entry stamped a FIXED windowOwedMinor = the whole
gap to window-open (e.g. 800 ALL for a 13:21 arrival to a 20:00 window) and
deferred it to exit. That over-charged anyone who left before the window
opened — a 1-hour visit was billed as 6.5 hours.

The amount isn't knowable at entry: a subscriber may enter early, leave after
an hour, come and go several times before the window opens, and linger past
window-close. They should pay only for the minutes actually parked outside the
window (capped at the window edges) — exactly what minutesOutsideWindow already
computes.

So the entry now stamps a MARKER only (outOfWindow: true + windowTariffVersionId
for reproducible pricing), no fixed amount. The exit gate and booth quote price
it live via windowOwedBetween(entry → settle-time), which already caps at the
window edges (early entry stops accruing at window-open; the in-window portion
of a crossing stay is free; the late-exit tail keeps accruing until payment).
Both already called that one function, so they agree.

- subscription-flow: entry stamps outOfWindow marker; the advisory slip is now a
  scannable out-of-window TICKET (Code128 + QR of the occurrence id).
- shared LedgerPayload: add outOfWindow; mark windowOwedMinor/windowGap*/
  windowCurrency deprecated read-only (historic signed events still type-check).
- BoothScreen: window-charge badge keys on outOfWindow (or the old stamp).
- ActiveSessions: drop the always-on "Open barrier" for subscribers — the
  assist-open / window-charge payment live in the pay modal, so the list can't
  one-click past an unpaid out-of-window charge.

Verified the live model on a DB copy: 13:21→14:30 = 200 ALL; 19:55(in grace)→
23:00 = 0; 19:00→21:30 (crosses into window) = 100 ALL. Existing signed
occurrences left untouched (immutable). build+lint 14/14, shared 87/87.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-21 13:34:35 +02:00
parent df5caf8d87
commit 8acef0464c
6 changed files with 113 additions and 56 deletions
+16
View File
@@ -1229,3 +1229,19 @@ keypair generated: pubkey embedded in tauri.conf.json; private key + password ke
.deb/.rpm/.AppImage + .sig updater signatures; turbo run build lint 14/14 green; no key material in
the repo. Updated As-built in [[desktop-shell-tauri]]. Deferred: real update URL, OS installer
signing, Windows kiosk-browser fallback.
## [2026-06-21] fix | Subscription out-of-window charge — marker-not-fixed-amount; scannable ticket; booth flow
Corrected the [[subscription]] tariff-bridge charging model after operator feedback. The entry path
stamped a FIXED `windowOwedMinor` = the whole gap-to-window-open (e.g. 800 ALL for a 13:21 arrival to
a 20:00 window) and deferred it — over-charging anyone who left before the window opened (a 1-hour
visit billed as 6.5h). Now the `vehicle_entry` carries only a MARKER (`outOfWindow` +
`windowTariffVersionId`); the amount is priced LIVE from `minutesOutsideWindow(entry → settle-time)`,
which caps at the window edges, so one hour parked = one hour's transient fee, in-window time free, and
the late-exit tail keeps accruing until payment. The advisory slip is now a scannable Code128 + QR
TICKET of the occurrence id (operator scans it into the booth pay modal). Also: removed the always-on
"Open barrier" from the active-sessions list AND modal for subscribers — a prepaid sub shows only a
small "assist open" reveal; an out-of-window sub is pay-first-then-open. Fixed the ESC/POS encoder so
typographic chars (— ⚠ … ' ") transliterate to ASCII instead of "?". `windowOwedMinor`/`windowGap*`
kept as deprecated read-only in `LedgerPayload` for historic events. Verified live model on a DB copy
(13:21→14:30 = 200 ALL; 19:55-grace→23:00 = 0; 19:00→21:30-cross = 100 ALL). build+lint 14/14, shared
87/87. Existing signed occurrences left untouched (immutable). See [[subscription]] tariff-bridge-history.