import { eq, siteConfig, type Db } from "@parking/db"; import { printWithFailover, registry, type PrinterDevice, type PrinterInstance, type TicketData, type TicketHeader, } from "@parking/devices"; import type { FastifyBaseLogger } from "fastify"; import { devicesByDirection } from "./device-resolve.js"; // Booth-side printing for the EXIT VOUCHER ("biletë dalje"). When the booth is far // from the exit, the customer pays at the booth and walks a printed voucher to the // exit, where they self-scan it. The voucher reprints the SAME ticket id as a // Code128 barcode (now a paid session) — so the exit reader runs the normal exit // validation and opens. See wiki/concepts/booth-exit-flow.md, ticket-encoding.md. // // This mirrors the entry flow's printer selection + header build, but prints on the // BOOTH printer (role "booth-receipt") since that's where the operator stands. /** Park identity for the voucher header, from site_config (all fields optional). */ function ticketHeader(db: Db): TicketHeader | undefined { const row = db.select().from(siteConfig).where(eq(siteConfig.id, 1)).get(); if (!row) return undefined; return { parkName: row.parkName, operatorName: row.operatorName, nius: row.nius, address: row.address, phone: row.phone, }; } /** Build live printer instances for failover selection (entry direction covers the * booth-receipt role too — the booth printer is configured on the entry side). */ function loadPrinters(db: Db): PrinterInstance[] { const rows = devicesByDirection(db, "printer", "entry"); const out: PrinterInstance[] = []; for (const row of rows) { const driver = registry.get(row.driverId); if (!driver) continue; const cfg = row.config as Record; const role = cfg.role === "booth-receipt" ? "booth-receipt" : "entry-dispenser"; try { out.push({ id: row.id, role, failoverRank: typeof cfg.failoverRank === "number" ? cfg.failoverRank : 0, device: driver.create(cfg as never) as PrinterDevice, }); } catch { // skip a printer whose config won't build } } return out; } /** * Print an exit voucher for a paid session: the same ticket id reprinted as a * barcode, on the booth printer (failing over to the entry dispenser). Returns the * id of the printer that printed it. Throws NoPrinterAvailableError if none can. */ export async function printExitVoucher( db: Db, ticketId: string, logger: FastifyBaseLogger, ): Promise { const printers = loadPrinters(db); const ticket: TicketData = { ticketId, issuedAt: new Date().toISOString(), header: ticketHeader(db), }; // Prefer the booth printer (operator is at the booth); fall back to the dispenser. const printedBy = await printWithFailover(printers, "booth-receipt", (d: PrinterDevice) => d.printTicket(ticket), ); logger.info(`exit voucher for ${ticketId} printed on ${printedBy}`); return printedBy; } /** * Print a SUBSCRIPTION CARD on the booth printer (failing over to the dispenser): * a scannable QR of the credential code + holder/validity, so the operator can hand * it to the customer. Used on subscription creation and on a "reprint" action. * Returns the printer that printed it; throws NoPrinterAvailableError if none can. */ export async function printSubscriptionCard( db: Db, card: { code: string; holderName?: string | null; validFrom?: string | null; validTo?: string | null }, logger: FastifyBaseLogger, ): Promise { const printers = loadPrinters(db); const data = { code: card.code, holderName: card.holderName ?? null, validFrom: card.validFrom ?? null, validTo: card.validTo ?? null, header: ticketHeader(db), }; const printedBy = await printWithFailover(printers, "booth-receipt", (d: PrinterDevice) => d.printSubscriptionCard(data), ); logger.info(`subscription card ${card.code} printed on ${printedBy}`); return printedBy; }