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, domain, business, shifts, anti-fraud]
|
||||
sources: []
|
||||
updated: 2026-06-19
|
||||
updated: 2026-06-20
|
||||
status: open
|
||||
---
|
||||
|
||||
@@ -64,8 +64,11 @@ login ————————————————————————
|
||||
|
||||
1. Determine the shift's payment set: the signed `payment` events ([[parking-session]],
|
||||
[[append-only-event-chain]]) between this shift's start mark and now. This includes a
|
||||
**[[subscription]] fee** an operator collects during the shift (sold/renewed at the booth → a
|
||||
signed `payment`, deferred build) — it folds into this set like any transient taking.
|
||||
**[[subscription]] sale fee** an operator collects during the shift (selling/renewing at the booth
|
||||
appends a signed `payment` with `subscriptionSale: true`, amount `priceMinor × months` — **built
|
||||
2026-06-20**) — it folds into this set like any transient taking, no special-casing. *(Before that
|
||||
date subscription sales appended nothing, so the cash was off the Z-report entirely — a real
|
||||
[[threat-model]] hole; see [[subscription]] "Collecting the fee".)*
|
||||
2. Sum by **tender**: `cashTotal`, and `cardTotal` from the POS/terminal **if a POS is configured**
|
||||
(the card line is omitted when there's no terminal).
|
||||
3. Append a signed **`shift_z_report`** event (type already in `packages/shared`): `{ operator,
|
||||
|
||||
@@ -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