wiki: design the business layer (session, tariff, permit, vision, shift, ops)

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).
This commit is contained in:
2026-06-15 17:41:38 +02:00
parent 2ab5a39a57
commit 8a8e74561d
21 changed files with 1173 additions and 12 deletions
+114
View File
@@ -355,3 +355,117 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
`localAddress` set. Root cause noted as Windows-side (stray 192.168.1.x); this is the
self-contained Linux answer.
- Updated [[wsl-dev-networking]].
## [2026-06-15] design | Business layer kickoff — parking session model
- Pivoted from the (hardware-verified) device/integrity layer to the business domain. Wiki-first.
- KEY DECISION: a [[parking-session]] is a PROJECTION over the signed [[append-only-event-chain]],
never a mutable table — a mutable sessions row with paid/owed would reopen the operator-fraud
hole the whole system closes. "Paid" = a signed `payment` event (unforgeable, undeletable).
- Scope (user): mixed site, TRANSIENT-FIRST; [[permit]] holders layered as a 2nd identity source
that short-circuits payment. Payment = PAY-ON-FOOT / pay station (decoupled from exit; exit lane
only validates paid + within walk-back grace). Matches [[autonomous-direction]].
- New signed event types designed (not yet built): `vehicle_entry`, `vehicle_exit`, `payment`,
`void` — extend `input_received`. Lifecycle OPEN→PAID→CLOSED (+VOIDED); overstay top-up is the
one genuinely stateful edge case.
- New pages: [[parking-session]], [[tariff]] (pure/data-driven fee fn; gracePeriodExit is a real
pay-on-foot revenue param), decision [[session-model]]. Updated [[append-only-event-chain]],
[[index]]. Closes the dangling entry-flow thread from [[device-input-flow]].
- [[permit]] drafted + RESOLVED from user input: credentials = RF tag/chip/card + QR (optical
reader). Car limits = two numbers: `registeredCars[]` whitelist + admin-set `maxConcurrent` (in
at once) — enforced as a fold over the permit's open sessions. Identity = card/QR OR matching
plate (either opens; card-sharing not prevented by design, caught by reconciliation). Autonomy =
host-in-the-loop for everything → Dingtian stays sufficient, no new controller; permit entry
fails closed if host down. Remaining open: reader hardware models; lapsed/revoked policy.
- NEXT: schema (`packages/db`: permits/tariffs + session projection) + the
input_received→vehicle_entry flow (closes the [[device-input-flow]] thread).
## [2026-06-15] design | Host-side vision service (ANPR + vehicle verification)
- User: optionally bind camera images to an OpenCV service we build. Resolved scope: ANPR (plate →
`IdentitySource='lpr'`); a **separate local Python/OpenCV microservice** on the appliance (Node →
localhost HTTP), offline; it **replaces the dedicated edge-AI [[lpr-camera]]** (recognition on
ordinary Hikvision/Dahua snapshots — reuses `Snapshot.bytes`).
- LICENSING: best ANPR/vehicle models are AGPL/commercial vs. the MIT/Apache/BSD standing rule.
Decision: **scoped AGPL exception** — allowed INSIDE the vision service only (separate process,
not linked); app stays permissive. Amended [[standing-decisions]].
- USER ANTI-FRAUD INSIGHT: a fraudster can print a registered plate and enter with a different car.
→ service also does **vehicle-attribute / fingerprint verification**, so the *car* reconciles, not
just the plate. This fills the independent-witness gap [[append-only-event-chain]] calls out:
plate-on-different-car = anomaly. Recognition is advisory (confidence + ticket fallback), evidence
(read + image) attaches to the signed event.
- New pages: [[opencv-anpr-service]], decision [[vision-service]]. Updated [[standing-decisions]],
[[lpr-camera]] (host-side supersedes edge-AI), [[permit]] (plate-spoof defence),
[[append-only-event-chain]] (vision as witness), [[index]].
- Open: recognizer/vehicle-model choice + accuracy; fingerprint method + anomaly threshold; appliance
compute (CPU vs GPU/NPU); per-camera opt-in; the still-unbuilt reconciliation logic.
## [2026-06-15] design | Transient pricing — composable, versioned tariff
- User: pricing is unknown + constantly changing → must be **admin-composable at runtime**, currency
selectable, FX later. Reframed [[tariff]] from "config we ship with numbers" to a first-class
editable entity.
- DECISIONS: (1) rate structure = **stepped duration blocks + rolling-24h daily cap** (flat rate is
one block; expresses first-hour/taper/cap with no special cases); (2) overstay top-up =
**reprice the difference** (recompute entry→now − alreadyPaid); (3) tariffs are **effective-dated
immutable versions** — edits publish a new version, sessions reprice against the version in force,
the `payment` event records `tariffVersionId` (reproducible + fixed in the signed chain); (4)
**one active tariff per site**, but modelled with id/scope so multi-tariff needs no migration;
(5) **currency selectable (ISO 4217)**, money = `{minorUnits, currency}`, payment reserves a null
`fxRate` → FX-ready, **FX engine deferred** (needs offline rate source — new [[open-questions]] #8).
- Ships with **no rate card**; owner must compose+publish one (blank = free or gated, operator
policy — open). Numbers in the page are illustrative, not defaults.
- Wrote the pure integer fee algorithm into [[tariff]] (data model: `tariffs` + immutable
`tariff_versions`). Updated [[open-questions]] (#8 FX), [[index]].
- NEXT: schema (`packages/db`) for tariffs/versions + permits + session projection, then the
composer UI + the input_received→vehicle_entry flow.
## [2026-06-15] design | Shifts (manned-only) + Z-report; drop time-based token
- Q: what happens at operator shift end? Resolved scope, deliberately small.
- Shifts exist ONLY in manned mode — a human accountability boundary. The fully-automated/unmanned
system has NO shifts; the pay-station cash-collection cycle + [[reconciliation]] replace it.
- Shift is NOT time-based: relief arrives late / no-shows / one operator forced into a double.
→ **drop the 8h token expiry**; login valid **until explicit logout** (updated [[local-jwt-auth]];
code change pending). Start/End Shift are **explicit, independent of login** — one login spans many
shifts; a double = End then Start again, no re-login.
- End Shift = sum signed `payment` events in the shift by tender → append a signed `shift_z_report`
(type already in packages/shared, chained to prior Z) → **PRINT cash total + POS total (if a POS
is configured)**. That's the whole human-side ask. No blind count / variance gate / manager
override. Fraud control stays in the signed chain + later [[reconciliation]] (catch a skim after
the fact, not at close). Blind-count documented as an explicit optional add-on, not built.
- New page [[shift]]; updated [[local-jwt-auth]], [[index]].
- Open: Z sums by payment-time (the operator who took the money) — confirm; X-report (read-only
mid-shift); per-operator vs per-booth vs per-site (ties to [[open-questions]] #1). `payment` event
needs a `tender` field (cash/card) — fold into the schema step.
## [2026-06-15] design | Scope sweep — capacity, validation, reporting, integrity gaps
- "What else can a PMS do?" — swept the full feature surface against the design; user picked the
in-scope gaps. New pages:
- [[capacity-occupancy]] — occupancy = fold over open sessions; refuse entry + drive a FULL sign
when full; **exit never blocked** ([[fail-state-safety]]); zone-ready; counting-drift = anomaly.
- [[validation-discounts]] — merchant validates a ticket → **signed discount event** applied at
fee time ([[tariff]]); over-validation visible to [[reconciliation]]; payment records gross/disc/net.
- [[reporting-analytics]] — revenue/occupancy/stay/permit/anomaly reports as projections over the
chain; **plate-search** (admin looks up a session by plate IF captured — honest "not captured").
- [[clock-integrity]] — fees depend on the host clock; offline box → backdating attack; monotonic
index catches reorder, clock-regression = `anomaly`, RTC + privileged-only time change.
- [[blocklist]] — barred plates/cards refused at **entry only**; signed + attributed.
- Folded into existing pages: **manual overrides** = signed reason-coded events (legitimate
counterpart to the out-of-band-open anomaly) + **lost-ticket admin-arbitrary amount** →
[[parking-session]] + [[tariff]]; **backup/restore** confirmed in-scope, expanded [[open-questions]]
#5 (restored copy must still verifyChain; doubles as the reconciliation export).
- NOT captured (flagged): **intercom/help-call** — user didn't select it, but it's the only human
fallback for an unmanned lane; revisit. Deferred roadmap: reservations, mobile app, EV, loyalty.
- Updated [[index]].
## [2026-06-15] design | Second sweep — ticket encoding + anti-passback; money corners deferred
- More gap-hunting. New pages:
- [[ticket-encoding]] — the transient session key: opaque/unguessable **ticket id printed as QR**
by [[rongta-printer]], **scanned at pay station + exit** (new ReaderDevice/imager behind the
adapter); plate-as-ticket ticketless alt coexists per lane. The physical backbone of the
transient flow (was only implied).
- [[anti-passback]] — one id can't enter while it already has an OPEN session (card/ticket-passing
over the fence); a fold over the chain, *under* permit `maxConcurrent`. Soft (flag `anomaly`) by
default vs. hard (refuse); honest dependence on reliable exit detection.
- DEFERRED (user): **receipts/VAT invoices** + **refunds/change/overpay** — depend on pay-station
hardware + manned/unmanned payment subsystem; recorded as [[open-questions]] #9, revisit at
procurement (may change what the `payment` event stores → flagged before schema).
- Still open & load-bearing: **lane topology** (#1) — not resolved; scopes sessions/occupancy/shifts.
- Updated [[open-questions]] (#9), [[index]].