import { eq, ledgerEvents, siteConfig, type Db } from "@parking/db"; // Occupancy = a FOLD over the signed ledger: the count of vehicle_entry events // with no matching vehicle_exit. Never a hand-maintained counter (which is // editable + drifts) — the chain is the truth. See wiki/concepts/capacity-occupancy.md. export interface Occupancy { /** Cars currently inside (open sessions). */ readonly count: number; /** Admin-set nominal capacity, or null = no limit. */ readonly capacity: number | null; /** capacity − count, or null when uncapped. Can read 0 (or below) when full. */ readonly free: number | null; /** True when count ≥ capacity (always false when uncapped). */ readonly full: boolean; } /** Count cars inside: entries minus exits, per identity, over the ledger. */ export function occupancyCount(db: Db): number { const rows = db .select({ type: ledgerEvents.type, identity: ledgerEvents.identity }) .from(ledgerEvents) .all(); const balance = new Map(); for (const r of rows) { if (r.type === "vehicle_entry") balance.set(r.identity ?? "", (balance.get(r.identity ?? "") ?? 0) + 1); else if (r.type === "vehicle_exit") balance.set(r.identity ?? "", (balance.get(r.identity ?? "") ?? 0) - 1); } let open = 0; for (const v of balance.values()) if (v > 0) open += 1; return open; } /** Admin-set capacity (null = uncapped). */ export function siteCapacity(db: Db): number | null { const row = db.select().from(siteConfig).where(eq(siteConfig.id, 1)).get(); return row?.capacity ?? null; } export function getOccupancy(db: Db): Occupancy { const count = occupancyCount(db); const capacity = siteCapacity(db); return { count, capacity, free: capacity == null ? null : capacity - count, full: capacity != null && count >= capacity, }; }