import type { Db } from "@parking/db"; import type { FastifyBaseLogger } from "fastify"; import type { DeviceReadEvent, ReadOutcome } 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 { const lane = await readerLaneWithAccess(this.#db, e.deviceId); if (lane == null) { return { accepted: false, reason: "reader not on an access-equipped lane" }; } const permit = this.#permit.match(e); if (permit) { return this.#permit.run(lane, e, permit); } // Not a permit → transient ticket exit (the exit flow rejects+logs if unknown). return this.#exit.handleAt(lane, e); } }