--- type: concept tags: [parking, domain, business, anti-fraud] sources: [] updated: 2026-06-15 status: open --- # Parking Session The core business-domain entity: one vehicle's stay, from entry to exit, plus the money owed and paid for it. Everything on the business side — [[tariff|tariffs]], payment, [[reconciliation]], revenue reporting — hangs off the session. This page defines what a session **is** and, just as importantly, what it is **not**. > Scope decision (2026-06-15): build the **transient** (casual, pay-for-duration) session first; > layer **permit holders** on top as a second identity source that short-circuits payment. Mixed > site, transient-first — see [[entry-exit-readers]] ("two populations, one shared relay") and > [[session-model]]. ## A session is a PROJECTION over the signed event log — not a mutable table This is the single most important rule, and it falls straight out of the [[threat-model]] (the adversary is the insider who can edit the database) and the [[append-only-event-chain]]: - The **events** table is the ledger and the **only** source of truth. `vehicle_entry`, `vehicle_exit`, `payment`, `void` are all **appended + signed**, never updated or deleted. - A **session** is a **read-model folded from those events** — open when an entry has no matching exit, paid when a `payment` event references it, closed when an exit lands. It MAY be cached in a table for query speed (dashboards, "cars currently in"), but that cache is **always rebuildable from the chain and never authoritative**. Same pattern as the `LaneMap` ([[append-only-event-chain]]), scaled to the business domain. - **Why this matters:** a mutable `sessions` row that stored "amount owed / paid" would reopen exactly the fraud hole the whole system exists to close (operator marks a session paid, pockets the cash). With sessions as a projection, "paid" is a **signed `payment` event** an operator can't forge or silently delete — a deletion breaks the chain visibly. See [[session-model]] for the rejected mutable-table alternative. ## Identity — how an entry is tied to its exit A session needs a key that survives from entry to exit. Two populations, two keys ([[entry-exit-readers]]): - **Transient:** a **ticket id** (printed, ideally on pre-numbered stock — see [[reconciliation]]) or a **plate** read by [[lpr-camera|LPR]]. This id is carried in the event's `identity` field. - **Permit holder:** a **credential** (card / plate / QR) matched to a [[permit]] record. A valid permit means the session owes nothing — the PAY step is skipped (see below). ## Lifecycle (pay-on-foot / pay station model) Payment is **decoupled from exit** (decision 2026-06-15, matching the [[autonomous-direction| unmanned]] roadmap): the customer pays at a central station before walking back to the car; the exit lane only *validates* that the session is settled. ``` ENTRY (lane) vehicle_entry event → session OPEN (ticket printed / plate read; barrier opens) PAY (pay station) payment event {sessionRef, fee, paidAt} → session PAID (grace window starts) EXIT (lane) validate: PAID && now ≤ paidAt + graceMinutes ? yes → vehicle_exit event → session CLOSED → pulseOpen no → reject → re-pay overstay top-up at station, then exit ``` States, as derived from events: | State | Condition (over the event chain) | | --- | --- | | **OPEN** | a `vehicle_entry` with no later matching `vehicle_exit` | | **PAID** | OPEN + a `payment` event covering the fee due, within its grace window | | **CLOSED** | a matching `vehicle_exit` event exists | | **VOIDED** | a `void` event references the session (lost ticket written off, error correction) | Permit sessions skip PAID: a valid [[permit]] at exit is itself the authorization to close. ## Edge cases the model must name (not yet designed in full) - **Overstay after payment** — exited the grace window; needs a top-up payment. The one genuinely stateful rule; handled as a second `payment` event, fee = f(time since paid). - **Lost ticket** — no entry id to match. A default flat "lost ticket" fee (see [[tariff]]), **or an amount the admin sets at the moment** (operator judgement — e.g. they can establish entry time from [[opencv-anpr-service|plate]] capture or CCTV and charge accordingly, or apply a fixed penalty). Recorded as a `payment` (with the chosen amount + a reason) + a `void`/annotation so it reconciles; the admin-set amount is captured in the signed event, attributed. - **Manual override** — an operator/admin opens the barrier for a stuck or disputed car, or writes off a session, as a deliberate act. Each is a **signed, reason-coded event** (`barrier_open_command` / a void with reason) — so an override is *authorized and logged*, while an open with **no** such signed event remains the fraud signal ([[append-only-event-chain]]). The override is the legitimate counterpart to the out-of-band-open anomaly. - **Forced / fail-open exit** — barrier failed open ([[fail-state-safety]]): the vehicle leaves with **no `vehicle_exit`**. This is an open session that never closes — a **reconciliation anomaly by design** ([[append-only-event-chain]]'s "physical open with no signed command"), not something to paper over. (A *manual* override above is the signed, non-anomalous version.) - **Re-entry / never-exited** — stale open sessions (drove out tailgating, sensor missed). Surface as anomalies; never auto-close silently. ## What this unblocks (build order) The device layer left the entry flow dangling — `input_received` events land in the log and stop ([[device-input-flow]] "the entry flow itself is the next build"). The session domain is that next step: consume `input_received` / a reader event → mint a signed `vehicle_entry` → print + open. Then the pay-station and exit-validation flows. Schema + code follow this page and [[tariff]]; the decision is recorded in [[session-model]].