fix(subs): out-of-window charge was a phantom 12h span (4,100 ALL bug)
The tariff-bridge owed amount summed TWO charges — the early-entry gap + a "late-exit" gap — and the exit gap (outOfWindowGap edge:"exit") always measured back to the PREVIOUS window close, even for a subscriber still BEFORE their window. So a car that entered ~30 min early showed ~12h owed (4,100 ALL) the moment it was looked up, instead of ~100 ALL. Replace the two-gap sum with a single correct primitive, minutesOutsideWindow(timeframes, tz, from, to): the minutes within the actual stay [entry, now] that fall outside the allowed window (covering early entry AND late exit, bounded by the stay, weekend/off-days free). windowOwedBetween prices those minutes once as a transient stay (so increments + daily cap apply) against the tariff in force at entry. Both the exit gate (subscription-flow) and the booth quote (pay-station) now use this one source of truth — they can't disagree. Verified on the live occurrence: was 4,100 ALL, now 100 ALL (9 min outside → one increment). 87 shared tests (6 new regression cases incl. the phantom span). Build+lint 12/12. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -16,7 +16,7 @@ import type { DeviceReadEvent, ReadOutcome } from "./device-events.js";
|
||||
import type { EventLog } from "./event-log.js";
|
||||
import { type FlowDirection, type ResolvedRelay } from "./device-resolve.js";
|
||||
import { snapshotAsync } from "./snapshot.js";
|
||||
import { windowCharge } from "./subscription-window.js";
|
||||
import { windowCharge, windowOwedBetween } from "./subscription-window.js";
|
||||
import type { VisionClient } from "./vision-client.js";
|
||||
|
||||
// SUBSCRIPTION flow: a subscriber identified by card/QR/plate enters/exits without
|
||||
@@ -249,33 +249,26 @@ export class SubscriptionFlow {
|
||||
}
|
||||
|
||||
/**
|
||||
* Total out-of-window charge owed for an occurrence right now: the carried EARLY-ENTRY
|
||||
* charge (signed on the `vehicle_entry` payload as `windowOwedMinor`) + a fresh
|
||||
* LATE-EXIT charge (window-close→now). Pure read; the entry portion is on-chain truth,
|
||||
* the exit portion is recomputed each scan (it grows until they leave). Returns the sum
|
||||
* and the currency. A plan without timeframes yields 0.
|
||||
* Total out-of-window charge owed for an occurrence right now: the transient cost of the
|
||||
* minutes parked OUTSIDE the plan's window over the WHOLE stay `[entry, now]` — ONE
|
||||
* computation covering early entry AND late exit (not entry-gap + exit-gap, which
|
||||
* double-counts and lets the exit gap reach a previous day's close). A plan without
|
||||
* timeframes yields 0. Single source of truth shared with the booth quote.
|
||||
*/
|
||||
#windowOwed(
|
||||
occurrenceId: string,
|
||||
_subscriptionId: string,
|
||||
planVersionId: string | null,
|
||||
): { totalMinor: number; currency: string | null } {
|
||||
// Carried early-entry charge from the signed entry payload.
|
||||
const entryRow = this.#db
|
||||
.select()
|
||||
.from(ledgerEvents)
|
||||
.where(eq(ledgerEvents.identity, occurrenceId))
|
||||
.all()
|
||||
.find((r) => r.type === "vehicle_entry");
|
||||
const ep = (entryRow?.payload ?? {}) as { windowOwedMinor?: number; windowCurrency?: string };
|
||||
const entryOwed = typeof ep.windowOwedMinor === "number" ? ep.windowOwedMinor : 0;
|
||||
|
||||
// Fresh late-exit charge (window-close → now), priced transiently.
|
||||
const exitCh = windowCharge(this.#db, planVersionId, new Date().toISOString(), "exit");
|
||||
const exitOwed = exitCh?.amountMinor ?? 0;
|
||||
|
||||
const currency = ep.windowCurrency ?? exitCh?.currency ?? null;
|
||||
return { totalMinor: entryOwed + exitOwed, currency };
|
||||
if (!entryRow) return { totalMinor: 0, currency: null };
|
||||
const owed = windowOwedBetween(this.#db, planVersionId, entryRow.occurredAt, new Date().toISOString());
|
||||
return { totalMinor: owed?.amountMinor ?? 0, currency: owed?.currency ?? null };
|
||||
}
|
||||
|
||||
/** Sum of signed `payment` events keyed to this occurrence (what the subscriber has
|
||||
|
||||
Reference in New Issue
Block a user