fix: record subscription sale as a signed payment (close off-book hole)
Creating a priced subscription wrote only the mutable `subscriptions`
master row and appended NOTHING to the signed ledger — so the cash an
operator collected showed in the live feed, drawer, and shift Z-report
nowhere, leaving no signed trace. A booth operator could sell
subscriptions and pocket the money untraceably — the exact
operator-as-adversary path the append-only signed ledger exists to close.
Found live: 3 priced subscriptions (27,000 ALL) had zero payment events.
Selling a priced subscription now appends a signed `payment` event at
create time: amount = priceMinor x months (full multi-month prepay),
operator-chosen tender (cash->drawer / card->bank), payload
{ subscriptionSale: true, permitId, operator, months }. Folds into the
shift Z-report/drawer with no new summing logic; the feed badges it
"subscription sale" and resolves the holder name. The create response
returns the recorded { sale }; subscriptionRoutes now takes the EventLog
and ShiftService.
Not hard-gated on an open shift (a sale can happen outside the booth money
path) — it warns instead. The 3 historical off-book sales are not
back-fillable (append-only forbids forging dated events) — reconcile via
cash_movement or a Z-report note.
Verified against a copy of the live DB with the real signing modules:
signed payment appended, hash-chain still verifies, lands in shift cash
totals. Build + lint 12/12.
Wiki: subscription "Collecting the fee" deferred -> BUILT (+ the off-book
hole and why); shift sale-folds-in; threat-model worked example
("store the price != account for the sale").
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
type: concept
|
||||
tags: [parking, security, foundational]
|
||||
sources: [parking-system-architecture]
|
||||
updated: 2026-06-14
|
||||
updated: 2026-06-20
|
||||
---
|
||||
|
||||
# Threat Model
|
||||
@@ -36,6 +36,17 @@ The same reframing recurs at the device layer: the [[uhppote-controller]]'s real
|
||||
unauthenticated commands ([[uhppote-udp-protocol]]), addressed by detection
|
||||
([[event-log-ingestion]]) or prevention ([[esp32-custom-controller]]).
|
||||
|
||||
> **Worked example — "store the price" ≠ "account for the sale" (found + fixed 2026-06-20).** Every
|
||||
> money-taking action must append a signed `payment` event, or it is invisible to
|
||||
> [[reconciliation]]. A concrete miss: selling a [[subscription]] wrote only the mutable
|
||||
> `subscriptions` master row (the agreed *price*) and **appended nothing to the ledger**, so the cash
|
||||
> the operator collected showed up in the feed/drawer/Z-report **nowhere** — a clean off-book channel
|
||||
> (three real sales, 27,000 ALL, untraceable). The fix is the textbook control: append a signed
|
||||
> `payment` (`subscriptionSale: true`) at sale time so it folds into the [[shift]] like any taking.
|
||||
> **The lesson generalises:** whenever a feature records *an amount* in a mutable table, ask "where is
|
||||
> the signed event that says money changed hands?" — a price in master data is not an accountable
|
||||
> transaction. See [[subscription]] "Collecting the fee".
|
||||
|
||||
> **Direction shift:** the system is heading toward **fully unmanned operation** — no operator, no
|
||||
> booth ([[autonomous-direction]]). That removes the booth-operator as the *primary* adversary, but
|
||||
> swaps in **unattended-machine threats** (tailgating, plate spoofing, physical tampering, forced
|
||||
|
||||
Reference in New Issue
Block a user