Files
parking_solution/wiki/concepts/ticket-encoding.md
T
julian 727c62da90 ticket: site metadata header + scannable Albanian ticket; widen barcode
- site_config gains optional park identity (park_name, operator_name, nius,
  address, phone, email); additive Drizzle migration 0001. GET/PUT
  /api/site-config read/write the full config (PUT partial patch, admin only);
  SiteSettings + SetupWizard expose the fields.
- renderTicket() prints an Albanian header sourced from site_config, the
  all-numeric 13-digit ticket id (12 random + Luhn) as Code128, large digits,
  and a lost-ticket footer. CP852 codepage so ë/ç render.
- Widen the Code128 module width 2->3 and height 80->100 dots so the
  short-range "Simple" QR/barcode reader decodes reliably (was barely reading
  at module width 2 on the 80mm head).

See wiki/concepts/site-metadata.md and ticket-encoding.md.
2026-06-17 12:17:21 +02:00

5.4 KiB

type, tags, sources, updated, status
type tags sources updated status
concept
parking
domain
business
devices
entry-flow
2026-06-15 open

Ticket Encoding & Scanning

How a transient parking-session's ticket id is printed, carried by the customer, and read back at the pay station and exit. This is the physical backbone of the transient flow — the thing that links entry → pay → exit when there's no plate.

The ticket id is the session key

At entry the system mints a vehicle_entry event with a ticket id (identity) and prints a ticket the customer keeps. That same id is read back later to find the session. Properties the id must have:

  • Opaque + unguessable — a random id (not a sequential count an attacker could iterate to claim someone else's cheaper session). Sequential physical stock numbering is a separate reconciliation aid (reconciliation pre-numbered stock), not the scan key.
  • All-numeric (as-built 2026-06-17) — so ANY legacy 1D barcode scanner reads it and an operator can hand-key it. Random (not sequential), so "all-numeric" does not weaken the unguessable property. Format: 13 digits = 12 cryptographically-random digits + 1 Luhn check digit (10^12 space → negligible collisions at lot scale; the Luhn digit lets manual entry reject a typo rather than fail as "session not found"). newTicketId() in apps/server/src/entry-flow.ts; validate with validateTicketCode() (gate MANUAL entry only — a scanned/looked-up id already in the ledger is authoritative regardless of format).
  • Format is a property of minting, not the schema — identity / sessions.id are free-form text, so changing the id format is a code change with no migration. Legacy T-<uuid> ids (pre-2026-06-17) remain valid keys and coexist with numeric ones.
  • Single logical session — scanning it at the pay station finds the open session; after payment it's the proof-of-paid the exit checks.

Encoding: Code128 numeric barcode — printed by the booth dispenser

  • The rongta-printer prints the ticket id as a 1D Code128 barcode (the all-numeric code), with the same code in large human-readable digits below it, then the entry time. Code128 over QR for the primary symbology because the booth's reader hardware is unknown and a legacy 1D laser scanner is the lowest common denominator — and the printed digits mean total reader failure still leaves a hand-keyable code. A QR for phone/imager scanning may be added later behind an admin toggle (deferred — see Open).

As-built (2026-06-17). renderTicket() in rongta-printer (packages/devices/src/drivers/printer-rongta.ts) emits the Code128 via ESC/POS GS k (code set B) — rendered by the printer firmware, so there is no image-rendering step and no new dependency (keeps the MIT/Apache/BSD constraint). Resilience rationale: the booth's reader is uncertain, so the id is carried in two independently-readable forms (1D barcode / printed digits). The "operator scans with a phone" path reuses the existing dispatch flow (entry-exit-readers) and is tracked separately (not yet built).

  • Scan points (both host-side reads — entry-exit-readers):
    • Pay station — customer scans the ticket → host finds the session → shows fee → takes payment (tariff, pay-on-foot) → appends payment.
    • Exit lane — customer scans the (now paid) ticket → host validates paid + within gracePeriodExit → vehicle_exit → pulseOpen.
  • The scanner is a device behind an adapter (device-adapter-pattern): a new ReaderDevice kind (QR/barcode imager) — likely the same IdentitySource = "ticket" / "qr" path. Keeps the app device-agnostic; hardware model is procurement (bom, open-questions).
    • On hand: the gee-qr-er80 QR access reader (-Q-W: QR scanner, Wiegand/RS-232/RS-485, Linux-supported) — the concrete scanner for this path. A serial ReaderDevice adapter feeds the read bus; pending the reader's RS-232 frame/baud (see gee-qr-er80 open questions).

Ticketless alternative (plate as the ticket)

Where the opencv-anpr-service/LPR captures the plate, the plate can be the session key instead of a printed ticket — drive in, plate read, drive to pay station and enter plate (or it's looked up), pay, exit by plate. No paper. The two can coexist per lane (entry-exit-readers "both share a relay"); a printed QR ticket is the fallback when a plate isn't captured or is low-confidence (recognition is advisory — opencv-anpr-service).

Open

  • Primary symbology decided: Code128 set B over the all-numeric id (as-built above). Still open: what else prints (site name, tariff summary, help number).
  • Optional QR (deferred) — an admin toggle to ALSO print a QR for phone/imager users. The code128()/qrCode() ESC/POS helpers were prototyped 2026-06-16; QR was dropped 2026-06-17 in favor of "1D barcode + hand-keyable numeric code" because the booth's reader hardware is unknown. Revisit when mobile scanning is wanted.
  • Phone-scan fallback (operator scans a ticket with a phone when a reader is down) — designed but not built: an authenticated route feeding the same dispatcher + a minimal mobile scan UI.
  • Scanner hardware (imager model; same unit at pay station and exit?).
  • Lost/damaged ticket → the lost-ticket path (parking-session, tariff admin-arbitrary amount).