feat(booth): overstay sessions, top-up pricing, and session/feed filters
Rework paid-but-grace-expired sessions and add booth filters. Overstay (was "stuck"): - Stop silently aging out a paid transient whose walk-back grace lapsed with no signed exit. Keep it listed with an OVERSTAY badge — a new parking period began (re-parked) or the car is faulty/abandoned; it is not a system fault. - No free exit: reopenBarrier refuses server-side once a transient's payment grace has expired (allow only subscription OR paid-and-within-grace); the UI hides the Open-barrier button on overstay rows and routes to the pay/exit modal. Closes a hole where a stale payment authorized a free multi-day exit (operator-as-adversary). - Price the overstay as a NEW period from grace-expiry -> now with its own daily-cap ladder, NOT "full stay minus paid" (which a daily cap collapsed to 0 — ticket 1245791632490 owed ALL 0; now owes its real overstay). quote() gains periodStart + overstay; SessionLookup/ActiveSession gain `overstay`. handlePayAndExit charges whenever the session is payable (was: only if !alreadyPaid, skipping the overstay). Filters (new ui/FilterBar): Active Sessions — search + status (unpaid/paid/exiting/overstay) + transient-vs-subscriber. Live feed — search + event (entry/exit/pay/void/anomaly) + direction + source (booth=manual vs reader). All client-side over already-fetched data; matched/total count shown. i18n parity (sq+en). Wiki: booth-exit-flow updated (overstay model, naming history, no-free-exit security fix, new-period pricing; open question on grace-renewal noted). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+55
@@ -948,3 +948,58 @@ Reworked the ANPR TRIGGER per the real design goal: when a transient pushes the
|
||||
## [2026-06-19] feat | Surface recognized plate in the booth UI (SnapshotStrip)
|
||||
|
||||
Made the ANPR plate VIEWABLE (it was saved but had no UI). Extended GET /api/snapshots/by-identity/:identity to also query device_events kind:"read" for that identity and return plates[] (plate, confidence, region, direction, snapshotId, at) alongside the existing snapshots + failures. The SnapshotStrip now renders each recognized plate as a cyan "Plate: AA558EE 100%" chip above the images (deduped by plate+direction; title shows region + time) — so it appears in BOTH the booth event-detail modal and the pay modal, beside the evidence photo, no separate screen. session:read gated (same as snapshots). i18n pay.plate sq+en. VERIFIED: by-identity returns plates[] for a seeded read (status 200, {plate:AA558EE, confidence:0.999, region:Albania, direction:entry, snapshotId}). Build+lint green. Updated [[opencv-anpr-service]].
|
||||
|
||||
## [2026-06-20] feat | Flag stuck sessions + booth filters (session list & live feed)
|
||||
|
||||
Replaced the silent paid age-out in PayStation.activeSessions() with a derived `stuck` flag: an
|
||||
open + paid + past-grace session with no signed vehicle_exit is no longer dropped — it stays listed
|
||||
with a red "stuck" badge so the operator can reconcile (top-up exit / void). Root cause surfaced via
|
||||
the live ledger: 4 such orphans (5717802544704, 1245791632490, 7985713986045, 9340902468934) each
|
||||
entry=1/exit=0/pay=1, grace=5min lapsed; they linger in the occupancy fold (so occupancy diverges
|
||||
from the active-list count) and a re-scan re-quotes the tariff from entry (paid customer charged
|
||||
again). Signed log untouched; subscriptions never stuck (no paidAt). This removed the earlier
|
||||
unbounded "presumed-left (N)" counter (occupancy − sessions), which had been growing.
|
||||
|
||||
Added a shared client-side FilterBar (ui/FilterBar.tsx: search + SegGroup toggles, matched/total
|
||||
count). Active Sessions: search (ticket/holder) + status (unpaid/paid/exiting/stuck) + transient-vs-
|
||||
subscriber. Live feed: search (identity/subscriber/plate) + event (entry/exit/pay/void/anomaly) +
|
||||
direction (entry/exit) + source (booth=manual vs reader=device). No new API. Exit grace re-scan
|
||||
logic UNCHANGED (still refuse + send to booth — user choice). Build+lint green across the monorepo.
|
||||
Updated [[booth-exit-flow]].
|
||||
|
||||
## [2026-06-20] fix | No free exit on overstay (stuck session) + top-up pricing
|
||||
|
||||
SECURITY FIX correcting same-day stuck-flag work. A stuck session (paid + walk-back grace expired +
|
||||
no signed exit) is AMBIGUOUS — the car may have left OR be overstaying inside. The first cut kept the
|
||||
"Open barrier" button on these rows (gated on paidAt != null), which would let an operator wave out a
|
||||
2-day overstay for free — the operator-as-adversary path. Fix: reopenBarrier now refuses a transient
|
||||
whose payment grace has expired (allow only subscription OR paid-and-within-grace), enforced
|
||||
server-side (exit-flow.ts), mirrored in the UI (no button on s.stuck → routes to pay/exit modal).
|
||||
Verified against the live ledger: ticket 5717802544704 (entered 73.9h ago, paid 200000, grace 5min)
|
||||
computes stuck=true and reopenBarrier REFUSES it.
|
||||
|
||||
Top-up pricing — "full stay minus paid" (user choice): quote() now returns grossMinor (whole stay
|
||||
entry→now) and paidMinor (fold of prior signed payment amounts), with amountMinor = max(0, gross −
|
||||
paid) — the delta only, never the full stay twice. lookup()/SessionLookup gained `stuck`; the pay
|
||||
modal shows OVERSTAY status + "Top-up due" + a hint, and canPay now allows payment for a stuck
|
||||
session. Taking the top-up restarts grace so the car exits normally. i18n pay.overstay/overstayHint/
|
||||
topUp in sq+en. Partially resolves the grace-overstay Open question (the amount); whether to bill the
|
||||
overstay delta-from-grace vs full-minus-paid left open. Build+lint green. Updated [[booth-exit-flow]].
|
||||
|
||||
## [2026-06-20] fix | Rename stuck→overstay + price overstay as a NEW period (fixes ALL 0)
|
||||
|
||||
Two user-driven corrections to the same-day overstay work. (1) NAMING: "stuck"/"i ngecur" wrongly
|
||||
implied a system fault trapping the customer — but a paid-then-grace-expired car means a NEW parking
|
||||
period began (re-parked) or the car is faulty/abandoned. Renamed the flag + badge + filter +
|
||||
SessionLookup/ActiveSession field to `overstay` / "tej afatit" across server + web + i18n.
|
||||
|
||||
(2) PRICING BUG: "full stay minus paid" collapsed to 0 under a daily cap — ticket 1245791632490
|
||||
(entered 06-17, paid 330000, cap 100000/day) had gross=330000, so delta=0 → "Diferenca për pagesë
|
||||
ALL 0", a free multi-day exit. Fix (user choice): quote() now prices an overstay as a NEW period
|
||||
anchored at grace-expiry (paidAt+graceExitMin)→now with its own daily-cap ladder, NOT entry→now. The
|
||||
tariff version stays the one frozen at entry. Quote gained periodStart + overstay; removed
|
||||
grossMinor/paidMinor. Verified: 1245791632490 now owes 20000 ALL (first half-hour of overstay), not 0.
|
||||
pay modal: "New period due"/OVERSTAY; handlePayAndExit now charges when canPay (was: only if
|
||||
!alreadyPaid — would have skipped the overstay charge). i18n pay.overstay/overstayHint/topUp +
|
||||
booth.badgeOverstay*/fStatusOverstay rewritten in sq+en. Build+lint green. Updated [[booth-exit-flow]]
|
||||
(overstay section + naming history + partial-resolution note on the grace-renewal open question).
|
||||
|
||||
Reference in New Issue
Block a user