fix(exit): stuck active session — paid ticket with no vehicle_exit

A paid car that left via a manual barrier re-open kept no vehicle_exit, so
activeSessions() saw it as permanently open and the grace-expiry eviction
(which only ran for exited sessions) never fired — it lingered forever
(ticket T-397815c0).

- reopenBarrier() now signs a vehicle_exit (source:manual) when the session
  is still open, closing it; still no second exit when already exited
  (phantom re-close — no double-count).
- activeSessions() ages out a PAID open session past grace even with no exit
  (unpaid open sessions never age out — a car owing money stays). Pure
  display filter; the signed log is untouched.

Verified both fixes + chain integrity on a fresh DB. A one-off corrective
vehicle_exit was appended to the live ledger to clear T-397815c0.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-18 13:14:45 +02:00
parent f87e4c0d6b
commit ca8c7f2fa2
3 changed files with 64 additions and 16 deletions
+23 -6
View File
@@ -2,7 +2,7 @@
type: concept
tags: [parking, domain, booth, exit, payment, threat-model]
sources: []
updated: 2026-06-17
updated: 2026-06-18
status: open
---
@@ -62,21 +62,38 @@ phantom obstacle: an animal, a person, a cardboard box or bag in the wind). Thes
**human in the booth** to open the barrier, leaving a signed trace.
**A session is "active" (shown in the booth Active Sessions list) while it is EITHER:**
- **open** — entered, no `vehicle_exit` yet (still inside), OR
- **open + unpaid** — entered, no `vehicle_exit`, owing money. **Always shown** — a car that owes
money never ages out; it's genuinely still inside until it pays, however long that takes. OR
- **open + paid, still within grace** — paid but no exit recorded yet, `now ≤ graceExpiresAt`. OR
- **exited but `now ≤ graceExpiresAt`** — paid and/or the voucher scanned, but still within the
walk-back grace window. Because the barrier is unconfirmed, the car is presumed *possibly still
present* until grace expires. **Payment and a successful voucher scan do NOT remove it from the
list** — only grace expiry does.
A session drops off the list once it is exited **and** past grace (presumed truly gone).
A session drops off the list once it is **past grace** and EITHER exited OR **paid** (presumed truly
gone). The **paid age-out** is important: a paid session whose walk-back grace lapsed has left, so it
is omitted **even if no `vehicle_exit` was ever signed**. Without this, a paid car that left via a
manual barrier re-open (which historically signed no exit — see below) would linger **forever**
(ticket T-397815c0, 2026-06-18). The signed log is untouched — this is purely the list's display
filter (`PayStation.activeSessions()`).
### The one operator action — "Open barrier" (audited re-pulse)
For an active session, the operator can open the barrier as a **human intervention**. This:
- **re-pulses an exit relay** (resolved site-wide, as the booth exit does), and
- signs an **`anomaly`** (`source: booth`, attributed to the operator, reason "manual barrier open")
— **NEVER a second `vehicle_exit`** (a second exit would double-count occupancy and corrupt the
ledger's meaning). It is an audited *re-open*, not a new exit.
- signs an **`anomaly`** (`source: booth`, attributed to the operator, reason "manual barrier open"), and
- **closes the session IF it is still open** — i.e. if no `vehicle_exit` exists yet, the re-open *is*
this car leaving, so it also signs a **`vehicle_exit`** (`source: manual`, reason "human-intervention
exit"). If the session is **already exited** (the phantom re-close case — a second exit would
double-count occupancy), it signs **no** second exit: anomaly only.
> **Refined 2026-06-18 (was "NEVER a `vehicle_exit`").** The original rule never signed an exit on a
> re-open, on the assumption a normal `vehicle_exit` had already happened. But when the re-open was the
> *only* way a car left (its walk-back grace had expired, so a normal exit was refused), the session
> kept **no exit event** and lingered as "open" forever (ticket T-397815c0). Fix: sign the exit only
> when the session is **still open**, preserving the no-double-count guarantee for the already-exited
> case. The [[#a-session-is-active|paid age-out]] above is the belt-and-braces safety net for any
> paid session that still slips through.
**Guard — no payment, no button.** The "Open barrier" action is shown/active **only for sessions that
have a payment** (paid, or paid-and-exited-in-grace). An **unpaid** open session has **no barrier-open