server: exit flow (pay-on-foot validation)
A credential read at an exit lane validates the session, then opens. Adds a 'read' channel to the device bus (DeviceReadEvent: ticket/plate/qr/card); entry stays button-driven so reads are exit/identity events. Flow: read -> fold the SIGNED ledger for that identity -> validate open + PAID + within gracePeriodExitMin -> signed vehicle_exit -> pulseOpen -> close the session cache. Unpaid / grace-expired / unknown -> signed anomaly, barrier stays closed (a deliberate business reject, not a fail-state; 'exit fails open' is about host/power loss). Validation reads the ledger (authoritative), not the cache. No payment events exist until the pay station is built, so every transient exit currently rejects -- the correct end-state, not yet passable. Verified against stubs: unpaid->anomaly+no-open; paid+grace->exit+open+closed; expired->anomaly; unknown->anomaly; verifyChain ok across entry->pay->exit. Flagged: lane_devices has no entry/exit direction model (exit door hardcoded to 1); needs a lane-direction/role model before multi-reader lanes.
This commit is contained in:
@@ -96,8 +96,25 @@ Permit sessions skip PAID: a valid [[permit]] at exit is itself the authorizatio
|
||||
|
||||
## What this unblocks (build order)
|
||||
|
||||
The device layer left the entry flow dangling — `input_received` events land in the log and stop
|
||||
([[device-input-flow]] "the entry flow itself is the next build"). The session domain is that next
|
||||
step: consume `input_received` / a reader event → mint a signed `vehicle_entry` → print + open.
|
||||
Then the pay-station and exit-validation flows. Schema + code follow this page and [[tariff]];
|
||||
the decision is recorded in [[session-model]].
|
||||
The device layer left the entry flow dangling — the session domain is that next step. Schema + code
|
||||
follow this page and [[tariff]]; the decision is recorded in [[session-model]].
|
||||
|
||||
### As-built (2026-06-15)
|
||||
|
||||
- **Entry flow** (`apps/server/src/entry-flow.ts`): access-device input edge → print ticket
|
||||
(failover) → signed `vehicle_entry` → `pulseOpen`. Holds (anomaly, no open, no entry) if printing
|
||||
fails. See [[device-input-flow]].
|
||||
- **Exit flow** (`apps/server/src/exit-flow.ts`): a credential **read** (new `read` bus channel) →
|
||||
fold the signed ledger for that identity → validate **open + PAID + within `gracePeriodExitMin`**
|
||||
→ signed `vehicle_exit` → `pulseOpen`. Unpaid / expired / unknown → signed `anomaly`, barrier
|
||||
stays closed. Validation folds the **ledger** (authoritative), then updates the `sessions` cache.
|
||||
- **Not a fail-state:** an unpaid reject keeps the barrier closed deliberately (driver returns to
|
||||
the pay station); "exit fails open" ([[fail-state-safety]]) is about the *system* being unable
|
||||
to decide (host/power loss), not an unpaid car.
|
||||
- **Currently every transient exit rejects** — no `payment` events exist until the pay station is
|
||||
built; the validation is the correct end-state, just not passable yet.
|
||||
|
||||
> **Design gap (flagged):** `lane_devices` has **no entry/exit direction** model. Entry is
|
||||
> button-driven and exit is read-driven, so they don't currently collide — but a lane with both an
|
||||
> entry reader and an exit reader can't yet be distinguished. A lane-direction/role model is needed
|
||||
> before multi-reader lanes (relates to [[open-questions]] #1 topology).
|
||||
|
||||
+17
@@ -500,3 +500,20 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
|
||||
new custody/session shape. Captured as [[valet-overcapacity]] + made [[capacity-occupancy]] FULL a
|
||||
soft policy; NOT built into the entry flow (clean seam left). Deferred.
|
||||
- New page [[valet-overcapacity]]; updated [[capacity-occupancy]], [[index]].
|
||||
|
||||
## [2026-06-15] build | Exit flow (pay-on-foot validation)
|
||||
- Built `apps/server/src/exit-flow.ts`. Added a `read` channel to the device bus (DeviceReadEvent:
|
||||
ticket/plate/qr/card) — readers/LPR emit reads; entry stays button-driven, so reads are
|
||||
unambiguously exit/identity events for now.
|
||||
- Flow: read → fold the SIGNED ledger for that identity → validate open + PAID + within
|
||||
`gracePeriodExitMin` → signed `vehicle_exit` → pulseOpen → close the session cache. Unpaid /
|
||||
grace-expired / unknown → signed `anomaly`, barrier stays closed (a deliberate business reject,
|
||||
NOT a fail-state; "exit fails open" is about host/power loss). Validation reads the ledger
|
||||
(authoritative), not the cache.
|
||||
- Pay station doesn't exist yet → no `payment` events → every transient exit currently REJECTS.
|
||||
Correct end-state, not passable until pay-station lands (decided).
|
||||
- VERIFIED against stubs: unpaid→anomaly+no-open; paid+grace→vehicle_exit+open+closed; grace-expired
|
||||
→anomaly; unknown ticket→anomaly; verifyChain ok across entry→pay→exit.
|
||||
- GAP flagged: lane_devices has no entry/exit DIRECTION model (door mapping hardcoded to 1 for exit);
|
||||
fine while entry=button/exit=read, but multi-reader lanes need a lane-direction/role model (ties to
|
||||
[[open-questions]] #1). Updated [[parking-session]] as-built + gap, [[index]].
|
||||
|
||||
Reference in New Issue
Block a user