--- type: concept tags: [parking, domain, business, occupancy] sources: [] updated: 2026-06-15 status: open --- # Capacity & Occupancy How many vehicles are inside, how many spaces remain, and what happens when the lot is full. ## Occupancy is a projection (like everything else) `occupancy = count(open [[parking-session|sessions]])` — an entry with no matching exit. It is a **fold over the signed [[append-only-event-chain]]**, never a hand-maintained counter (a counter is editable and drifts; the chain is the truth). Spaces-free = `capacity − occupancy`. - **`capacity`** is admin-set per site (and per **zone/level** if the lot has sections — model a `zone` on capacity + on the entry so multi-level is a later addition, not a rewrite). - Permit concurrency (`maxConcurrent`, see [[subscription]]) is the same kind of fold, scoped to one permit's open sessions. ## Full → refuse entry + FULL sign - When `occupancy ≥ capacity`, the entry flow **refuses** (no `vehicle_entry`, no barrier open) and can drive a **"FULL" sign** (a relay/output, via the device adapter layer). - **Safety/policy nuance:** "full" blocks *entry* only — **exit always works** ([[fail-state-safety]]: exit fails open; never trap a vehicle). Permit holders may be allowed in past a "transient full" threshold (reserve spaces for subscribers) — an optional policy knob. - **Counting drift is real:** tailgating (two cars, one entry) and missed reads make the live count diverge from physical reality. The count is the *system's* occupancy; periodic ground-truth (a loop count, or the [[opencv-anpr-service|vision]] count) reconciles it — surfaced as an anomaly, not silently corrected. ## Reserved subscriber spots (admin toggle, built 2026-06-20) By default occupancy counts only cars **physically inside** — a subscriber who isn't parked frees their spot to transients, and the operator handles any overflow by valet/key-juggling. A site can instead **hold a spot for every active subscriber**, so the lot reads "full" to transients sooner and the subscriber's place is guaranteed: - `site_config.reserve_subscriber_spots` (bool, default off). When ON, `reservedSubscriberSpots(db)` sums, over every **active** subscription (status active AND `now ∈ [validFrom, validTo]`), `max(0, quantity − itsCarsCurrentlyInside)` — i.e. it reserves only the **not-yet-parked** portion of each subscription's [[subscription|quantity]] (a parked subscriber already occupies a real spot; counting them twice would over-reserve). - `getOccupancy` gains `reserved` + `effectiveFree = capacity − count − reserved`. The transient FULL gate becomes **`count + reserved ≥ capacity`**. Subscribers are still **never** gated by full (their flow ignores it) — reservation only tightens the *transient* gate. - OFF = the prior behaviour exactly (`reserved = 0`). ## "Full" is a soft, operator-configurable policy Refusing at capacity is the **default**, not an absolute. An operator may opt into **[[valet-overcapacity|valet over-capacity]]** — accept the car into operator custody (keys handed over, stacked beyond the marked count) instead of refusing. So the FULL gate is a policy knob (refuse vs. valet-accept), set by the operator per site. Valet is a manned-mode feature with its own custody/session shape — see [[valet-overcapacity]] (deferred). ## As-built (2026-06-16) - **Occupancy** = `occupancyCount` (`apps/server/src/occupancy.ts`): a fold over the ledger — entries minus exits per identity, count those `> 0`. `getOccupancy` returns `{count, capacity, free, full}`. - **Capacity** is a single-row `site_config` table (admin-set; `null` = uncapped). Routes (`routes/site.ts`): `GET /api/occupancy` + `GET /api/site-config` (any role), `PUT /api/site-config` (admin; non-negative int or null). - **FULL gate** is in the **transient entry flow**: `occupancy.full` → refuse (no ticket, no `vehicle_entry`, no open) + signed `anomaly`. **Permit entry is NOT gated** here — subscribers are admitted past transient-full (their own `maxConcurrent` still applies); occupancy can read over-capacity (`free` negative) when permits enter a full lot, as intended. - **UI** `SiteSettings`: live occupancy + FULL badge (everyone); capacity editor (admin). - Verified: fill to cap → 3rd transient refused; permit still admitted past full; exit frees a slot; RBAC (operator can't set capacity); verifyChain ok. Physical FULL-sign relay output is **deferred** (needs a sign device). ## Open - Zone/level granularity at launch vs. single capacity number. - Reserve-for-permits **threshold** (a soft transient cap below the hard capacity) — currently permits are simply ungated; a tunable threshold is the richer version. - Physical FULL-sign relay output (a sign-device role). - The valet over-capacity mode + custody model ([[valet-overcapacity]]).