Pivot from the hardware/integrity layer to the parking operation. All wiki-only; no code yet. Core principle throughout: business entities are projections over the signed append-only event log, never mutable tables. New concepts: parking-session, tariff (composable/versioned, FX-ready), shift (manned-only Z-report), capacity-occupancy, validation-discounts, reporting-analytics, clock-integrity, ticket-encoding, anti-passback. New entities: permit, opencv-anpr-service, blocklist. Decisions: session-model, vision-service (host-side ANPR + vehicle verification; scoped AGPL exception for the isolated service). Updates: append-only-event-chain (new event types + vision witness), local-jwt-auth (drop 8h expiry -> until logout; code change pending), lpr-camera (host-side recognition supersedes edge-AI), standing-decisions (AGPL exception), open-questions (+FX, +pay-station money corners, backup). Deferred + flagged: intercom/help-call, receipts/refunds/change, FX engine, lane topology (#1).
6.3 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | |||||
|---|---|---|---|---|---|---|---|---|---|
| entity |
|
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
< maxConcurrentif car-bound) → signedvehicle_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).
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
- 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.
- Lapsed-mid-stay & revoked policy (fall back to transient tariff vs. refuse) — confirm with operator.