Files
parking_solution/wiki/entities/permit.md
T
julian c24d99b0f4 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.
2026-06-15 19:47:01 +02:00

7.7 KiB

type, tags, sources, updated, status
type tags sources updated status
entity
parking
domain
business
subscriptions
identity
2026-06-15 open

Permit (Subscription)

A subscription: a known holder authorized to enter/exit without paying per-stay, for a covered period. The second of the "two populations" (entry-exit-readers); a valid permit short-circuits the payment step of a parking-session (session-model). Transient is built first; permits layer on top.

Credentials (how a permit is presented) — confirmed with operator 2026-06-15

A permit is recognized by a credential read at the lane. Two kinds, mapping to the two identity paths:

  • RF tag / chip / card. An RFID/proximity credential. Read host-side (reader → host → pulseOpen): autonomy isn't required (resolved below), and the dingtian-relay has no onboard card list anyway, so there's no need to route RF into a controller. A Wiegand-out reader is still fine and keeps a future autonomous path open (entry-exit-readers), but isn't required.
  • QR code. Read by the optical reader — inherently host-side (entry-exit-readers: pure optical/network readers are invisible to a controller). Host decodes the QR → looks up the permit → decides.

Both feed the host as a reader event whose source is wiegand / qr (the IdentitySource already in the model) and whose value is the credential id.

Two optional, independent bindings — confirmed 2026-06-15

A permit has two constraints the admin may or may not apply, orthogonally. Either, both, or neither — the four combinations are all valid.

1. Car-count binding (default: 1)

  • Optional. By default a permit is bound to 1 car at a time. The admin may raise the limit (a household, a company fleet) or unbind it entirely (no cap on how many cars use it).
  • The limit is on cars inside at once (maxConcurrent), enforced over the parking-session projection: at entry, count the permit's currently-open sessions; if < maxConcurrent (or unbound) allow, else reject (allowance full). This is exactly why sessions-as-projection matters — "how many of this permit's cars are inside right now" is a fold over open entry/exit events, not a counter someone can edit.

2. Plate binding (default: off)

  • Optional. By default a permit is not plate-bound — any car may use it (identity is the card/QR). The admin may bind it to a set of specific licence plates.
  • When bound, an allowed plate is an accepted identity in its own right — a valid card/QR OR a matching plate opens the lane (either, not a second factor):
entry: read card/QR → find permit → car-count ok → open
   OR  LPR plate ∈ permit's bound plates → find permit → car-count ok → open
  • Accepted tradeoff: card-OR-plate is the most convenient but does not prevent card-sharing (a lent card still opens). Fine for a trusted permit population; the signed append-only-event-chain records exactly which credential/plate entered, so abuse is visible to reconciliation after the fact.
  • Plate-spoofing defence: a printed copy of a registered plate on a different car is caught not here but by the opencv-anpr-service's vehicle-attribute verification — the seen car must reconcile with the permit's known car, not just the plate string.

The two are independent: a plate-bound permit may have no car cap; a car-capped permit may accept any plate. The binding fields are simply absent/null when a constraint isn't applied.

Data model (first cut — to firm up with session-model)

A permits table (and supporting rows). Unlike the event log, reference/master data like permits is mutable (an admin grants/revokes/renews) — but every use of a permit still produces a signed vehicle_entry/vehicle_exit event in the append-only-event-chain, so the audit trail stays append-only even though the permit record itself is editable.

Field Notes
id, holderName/contact the subscriber
credentials[] one or more: { kind: 'rf' | 'qr', value }
maxConcurrent car-count binding; default 1, raise for fleets, or null = unbound
plates[] plate binding; default empty/false = any car; when set, these plates are accepted identities
validFrom, validTo coverage window
status active / suspended / revoked

Both bindings are nullable/empty by default — a bare permit is "1 car at a time, any plate, identified by its card/QR".

Interaction with the session model

  • Entry: credential read → permit lookup → valid (active, in window, plate allowed if plate-bound, concurrent cars < maxConcurrent if car-bound) → signed vehicle_entry (source = wiegand/qr/lpr), open barrier. No ticket, no fee. (A bare permit applies neither extra check — just active + in window.)
  • Exit: credential/plate read → matching open permit session → signed vehicle_exit, open. No payment required.
  • Lapsed mid-stay: permit expires while a car is parked → the uncovered time falls back to the transient tariff (edge case to design).
  • Revoked: a revoked permit fails the entry check → treated as transient (take a ticket) or refused, per policy (OPEN).

As-built (2026-06-15)

apps/server/src/permit-flow.ts, reached via the read dispatcher (read-dispatch.ts): a credential read routes to the permit flow if it matches a permit (card/QR credential, or a bound plate) — otherwise to the transient exit flow. So one read handler serves both populations (entry-exit-readers), disambiguated by what the credential is.

  • Direction is inferred from session state for that car — the read credential value is the per-car session key. No open session for that car → ENTRY (check maxConcurrent, sign vehicle_entry, open); an open session → EXIT (sign vehicle_exit, open, close). A fleet permit thus has one session per car concurrently, and anti-passback falls out (a re-read of an inside car is its exit, never a second entry).
  • maxConcurrent is enforced as a fold over the signed ledger — count the permit's vehicle_entry events whose car has no later exit; reject at the limit (null = unbound).
  • Validity (active + within validFrom/validTo) and plate-OR-card identity as designed. No ticket, no fee — the permit is the authorization; every use is still a signed ledger event carrying permitId.
  • Refusals (revoked / out-of-window / at-capacity) are signed anomaly events; the barrier stays closed. Verified end to end (entry, inferred exit, fleet cap, plate-bound, revoked, dispatch).

Resolved (2026-06-15)

  • Two optional bindings, independent: car-count (maxConcurrent, default 1, raisable or unbound) and plate-binding (plates[], default off = any car). Either, both, or neither.
  • Plate vs. credential: when plate-bound, card/QR OR matching plate — either is accepted identity (not a second factor); card-sharing not prevented by design, caught by reconciliation after.
  • Autonomy: host-in-the-loop for everything — no onboard card list needed, so the dingtian-relay stays sufficient (no new controller). Permit entry fails closed if the host is down (fail-state-safety). One code path for transient + permit.

Open questions

  1. Reader hardware — confirm the RF reader and the QR/optical reader models (procurement; relates to bom and open-questions). RF need not be Wiegand now that autonomy isn't required, but a Wiegand-out reader keeps options open.
  2. Lapsed-mid-stay & revoked policy (fall back to transient tariff vs. refuse) — confirm with operator.