A credential read now routes by what the credential IS: matches a permit
(card/QR credential or a bound plate) -> permit flow; else -> transient exit
flow. Lane resolved once (readerLaneWithAccess); ExitFlow.onRead -> handleAt so
the dispatcher owns lane resolution.
Permit direction is inferred from session state for that car (the read value is
the per-car session key): no open session -> ENTRY (enforce maxConcurrent, sign
vehicle_entry, open); open -> EXIT (sign vehicle_exit, open, close). Fleet
permit = one session per car; anti-passback falls out naturally.
maxConcurrent enforced as a fold over the signed ledger (null = unbound).
Validity window + status + plate-OR-card identity as designed. No ticket/fee;
every use is a signed event carrying permitId. Refusals (revoked / out-of-window
/ at-capacity) are signed anomalies, barrier stays closed.
Verified against stubs: card entry -> inferred exit; fleet cap 2 (F3 rejected
at 2/2, then admitted after F1 exits); plate-bound opens; revoked rejects;
unknown credential falls through to exit reject; verifyChain ok.
computeFee() in @parking/shared: pure integer fee over a TariffStructure
(stepped blocks, rolling-24h cap). Two edges fixed under test: grace uses RAW
duration (not rounded-up minutes), and the block ladder resets each 24h day.
PayStation + routes (GET /api/pay/quote, POST /api/pay): look up the open
session, resolve the active tariff version (latest effectiveFrom <= entry),
computeFee, append a signed payment event (amount/currency/tender/
tariffVersionId/graceExitMin). overrideMinor handles lost-ticket/dispute. PCI
stays out of the app: tender only records cash/card.
Verified end to end: entry -> quote (300 for 90min) -> pay -> exit opens and
closes the session, verifyChain ok.
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.