db: business-layer schema — ledger/device event split, tariffs, permits, sessions
Implements the wiki design in packages/db + packages/shared. Event split: rename events -> ledger_events (signed business ledger) and add device_events (unsigned telemetry). ledger_events gains a signed JSON payload (amount/tariffVersionId/sessionRef/tender…) + keyId; canonicalize() includes the payload via sorted-key serialization so business data is tamper-evident. Raw Dingtian input now writes device_events, not a signed input_received. New tables: tariffs + immutable tariff_versions (composable/versioned, currency + FX-ready), permits (+ permit_credentials, permit_plates; maxConcurrent default 1), blocklist, sessions (rebuildable projection cache — not a source of truth). shared: split ParkingEvent/Type into LedgerEvent/LedgerEventType + DeviceEventKind; add LedgerPayload, Tender, TariffStructure/TariffBlock. Regenerated a single baseline migration (no production chain data existed). Verified: chain appends + verifyChain ok; tampering a payment payload breaks the signature. Full repo builds (5/5).
This commit is contained in:
@@ -13,39 +13,106 @@ export type Direction = "entry" | "exit";
|
||||
export type IdentitySource = "wiegand" | "lpr" | "qr" | "ticket" | "manual";
|
||||
|
||||
/**
|
||||
* An append-only parking event. Records are never mutated; corrections are new
|
||||
* A signed business-LEDGER event. Records are never mutated; corrections are new
|
||||
* events. `prevHash` chains each event to the previous one; `signature` is the
|
||||
* ATECC608 signature over the event contents. See wiki/append-only-event-chain.
|
||||
* ATECC608 signature over the canonical contents (which INCLUDE `payload`).
|
||||
* Distinct from device telemetry — see wiki/decisions/event-streams-split.md.
|
||||
*/
|
||||
export interface ParkingEvent {
|
||||
export interface LedgerEvent {
|
||||
readonly id: string;
|
||||
readonly index: number;
|
||||
readonly type: ParkingEventType;
|
||||
readonly type: LedgerEventType;
|
||||
readonly direction: Direction | null;
|
||||
readonly lane: number;
|
||||
readonly source: IdentitySource | null;
|
||||
/** Card number, plate, ticket id, etc. — depends on `source`. */
|
||||
readonly identity: string | null;
|
||||
/** Type-specific business data (amount, tariffVersionId, sessionRef…). Signed. */
|
||||
readonly payload: LedgerPayload | null;
|
||||
readonly occurredAt: string; // ISO-8601
|
||||
/** Hash of the previous event in the chain (hex). Null only for genesis. */
|
||||
readonly prevHash: string | null;
|
||||
/** ATECC608 signature over the canonical event payload (hex). */
|
||||
readonly signature: string;
|
||||
/** Which signer/key produced `signature` (verifiable across a signer swap). */
|
||||
readonly keyId: string;
|
||||
}
|
||||
|
||||
export type ParkingEventType =
|
||||
// A raw device input (e.g. a Dingtian button press) was received and recorded.
|
||||
// NOT a confirmed entry — the richer `vehicle_entry` is appended later by the
|
||||
// entry flow once a ticket prints and the barrier is commanded.
|
||||
| "input_received"
|
||||
/** Business/accountability events that live in the SIGNED, hash-chained ledger. */
|
||||
export type LedgerEventType =
|
||||
| "vehicle_entry"
|
||||
| "vehicle_exit"
|
||||
| "payment"
|
||||
| "void"
|
||||
// Witness-grade: a host-commanded open, and an independently-observed open
|
||||
// (loop/sensor) — reconciled against each other.
|
||||
| "barrier_open_command"
|
||||
| "barrier_open_observed"
|
||||
| "shift_z_report"
|
||||
| "anomaly";
|
||||
|
||||
/** How money was tendered (for payment events + the shift Z-report). */
|
||||
export type Tender = "cash" | "card";
|
||||
|
||||
/**
|
||||
* Type-specific data carried on a ledger event's `payload`. All amounts are
|
||||
* integer minor units in the named currency — never floats. Fields are optional
|
||||
* because they're event-type-specific; the producer fills what applies.
|
||||
*/
|
||||
export interface LedgerPayload {
|
||||
/** The parking_session this event concerns (entry/exit/payment/void). */
|
||||
readonly sessionRef?: string;
|
||||
/** payment: amount in minor units, its currency, and how it was tendered. */
|
||||
readonly amountMinor?: number;
|
||||
readonly currency?: string;
|
||||
readonly tender?: Tender;
|
||||
/** payment: which tariff_version priced it (reproducible repricing). */
|
||||
readonly tariffVersionId?: string;
|
||||
/** payment: gross/discount/net split when a validation applied. */
|
||||
readonly grossMinor?: number;
|
||||
readonly discountMinor?: number;
|
||||
/** FX-ready, deferred: rate applied (null/absent now). See open-questions #8. */
|
||||
readonly fxRate?: number | null;
|
||||
/** void / anomaly / override: a human/machine reason code. */
|
||||
readonly reason?: string;
|
||||
/** plate/vehicle from the vision service (advisory). */
|
||||
readonly plate?: string;
|
||||
readonly plateConfidence?: number;
|
||||
/** Free-form for forward-compat without a schema change. */
|
||||
readonly [k: string]: unknown;
|
||||
}
|
||||
|
||||
/** Operational device telemetry — UNSIGNED, prunable. NOT the ledger. */
|
||||
export type DeviceEventKind = "input" | "relay" | "status" | "read" | "snapshot";
|
||||
|
||||
/**
|
||||
* The composable rate card stored in a tariff_version.structure. Pure data the
|
||||
* fee function interprets — no rates in code. Stepped duration blocks + caps/grace;
|
||||
* a flat rate is just one block. See wiki/concepts/tariff.md.
|
||||
*/
|
||||
export interface TariffStructure {
|
||||
/** Free if exited within this (drop-off/turnaround). */
|
||||
readonly gracePeriodEntryMin: number;
|
||||
/** Billing granularity; partial increments round UP. */
|
||||
readonly incrementMin: number;
|
||||
/** Consumed in order as duration accrues; last block may be open-ended. */
|
||||
readonly blocks: readonly TariffBlock[];
|
||||
/** Cap per rolling 24h (null = no cap). */
|
||||
readonly dailyCapMinor: number | null;
|
||||
/** Flat charge when there's no entry id (admin may override at the moment). */
|
||||
readonly lostTicketMinor: number;
|
||||
/** Pay-on-foot walk-back window: minutes after payment to reach the car. */
|
||||
readonly gracePeriodExitMin: number;
|
||||
/** How an overstay top-up is charged. "reprice" = recompute(entry→now) − paid. */
|
||||
readonly overstay: "reprice";
|
||||
}
|
||||
|
||||
export interface TariffBlock {
|
||||
/** Upper bound of this block in minutes; null = open-ended (thereafter). */
|
||||
readonly uptoMin: number | null;
|
||||
readonly priceMinorPerIncrement: number;
|
||||
}
|
||||
|
||||
export const ROLES: readonly Role[] = [
|
||||
"admin",
|
||||
"operator",
|
||||
|
||||
Reference in New Issue
Block a user