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:
2026-06-20 15:46:17 +02:00
parent cdb55a8652
commit a20400c2c5
11 changed files with 289 additions and 42 deletions
+7 -1
View File
@@ -525,15 +525,21 @@ export type SubscriptionInput = {
/** Months paid for: when set (with validFrom), validTo = validFrom + months. */
months?: number | null;
status?: Subscription["status"];
/** How the sale fee was tendered (cash → drawer, card → bank). Used at CREATE when a
* price is set (the sale appends a signed payment); ignored on update. */
tender?: "cash" | "card";
credentials: SubscriptionCredentialInput[];
plates: string[];
};
/** The create response = the saved subscription + the auto-print outcome. */
/** The create response = the saved subscription + the auto-print outcome, plus the
* recorded SALE (the signed payment) when a price was collected. */
export type SubscriptionCreated = Subscription & {
printed: boolean;
printedBy?: string;
printError?: string;
/** Present when a priced subscription was sold: the signed payment just appended. */
sale?: { amountMinor: number; currency: string | null; tender: "cash" | "card"; inShift: boolean };
};
export function fetchSubscriptions(): Promise<{ subscriptions: Subscription[] }> {