server: permit entry/exit branch + read dispatcher
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.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import type { Db } from "@parking/db";
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import type { DeviceReadEvent } from "./device-events.js";
|
||||
import type { ExitFlow } from "./exit-flow.js";
|
||||
import type { PermitFlow } from "./permit-flow.js";
|
||||
import { readerLaneWithAccess } from "./lane-map.js";
|
||||
|
||||
// Routes a credential read (ticket scan / plate / card) to the right flow. A read
|
||||
// can mean a permit entry/exit OR a transient exit, so we dispatch by WHAT the
|
||||
// credential is (decision 2026-06-15):
|
||||
// - matches a permit (card/QR/bound plate) → PERMIT flow (direction inferred from
|
||||
// the car's open-session state),
|
||||
// - else → transient EXIT flow (open ticket session → exit, else reject+log).
|
||||
// Lane is resolved once here; both flows act on a known access-equipped lane.
|
||||
|
||||
export class ReadDispatcher {
|
||||
readonly #db: Db;
|
||||
readonly #exit: ExitFlow;
|
||||
readonly #permit: PermitFlow;
|
||||
readonly #logger: FastifyBaseLogger;
|
||||
|
||||
constructor(db: Db, exit: ExitFlow, permit: PermitFlow, logger: FastifyBaseLogger) {
|
||||
this.#db = db;
|
||||
this.#exit = exit;
|
||||
this.#permit = permit;
|
||||
this.#logger = logger;
|
||||
}
|
||||
|
||||
async dispatch(e: DeviceReadEvent): Promise<void> {
|
||||
const lane = await readerLaneWithAccess(this.#db, e.deviceId);
|
||||
if (lane == null) return; // reader not on an access-equipped lane — ignore
|
||||
|
||||
const permit = this.#permit.match(e);
|
||||
if (permit) {
|
||||
await this.#permit.run(lane, e, permit);
|
||||
return;
|
||||
}
|
||||
// Not a permit → transient ticket exit (the exit flow rejects+logs if unknown).
|
||||
await this.#exit.handleAt(lane, e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user