From eafbc3ddbb2c31251e74cfc3b43bd044ee4e8daf Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 20 Jun 2026 20:32:12 +0200 Subject: [PATCH] feat(booth): badge subscriber out-of-window entries in the live feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A subscriber entering outside their plan's allowed window gets a deferred transient charge (windowOwedMinor, collected/gated at exit) — but it was SILENT at the booth: the entry showed as a plain subscriber pass with no hint money is owed, so the operator only discovers it at exit. Surface it: add a "out-of-window — owes fee" badge on any entry/exit event carrying windowOwedMinor > 0, so the operator sees immediately that this subscriber owes a fee. Also type the window-charge fields on LedgerPayload (were riding the open-ended index signature). Behaviour is otherwise unchanged and correct — verified the live "Mon Kukaleshi" entry: entered 20:29 local (before the 21:00 Mon–Sat window, grace 5m), owes 100 ALL for 18:29–18:55Z, stamped on the signed entry, still owed, gated at exit. Subscribers get no ticket by design. Build+lint 12/12. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- apps/web/src/BoothScreen.tsx | 3 +++ apps/web/src/lib/i18n/en.ts | 1 + apps/web/src/lib/i18n/sq.ts | 1 + packages/shared/src/index.ts | 9 +++++++++ 4 files changed, 14 insertions(+) diff --git a/apps/web/src/BoothScreen.tsx b/apps/web/src/BoothScreen.tsx index 60c1630..d81c8af 100644 --- a/apps/web/src/BoothScreen.tsx +++ b/apps/web/src/BoothScreen.tsx @@ -112,6 +112,9 @@ function eventBadges(p: LedgerEvent["payload"]): string[] { if (p.permitRefused) keys.push("booth.badgeSubRefused"); if (p.ticketPrinted === false) keys.push("booth.badgeNoTicket"); if (p.subscriptionSale) keys.push("booth.badgeSubSale"); + // Subscriber entered/exited outside their plan's allowed window → owes a deferred + // transient charge, collected (gated) at exit. Flag it so the operator KNOWS now. + if (typeof p.windowOwedMinor === "number" && p.windowOwedMinor > 0) keys.push("booth.badgeWindowCharge"); if (p.source === "manual" && !p.subscriptionSale) keys.push("booth.badgeManualOpen"); return keys; } diff --git a/apps/web/src/lib/i18n/en.ts b/apps/web/src/lib/i18n/en.ts index 74e8590..488a08c 100644 --- a/apps/web/src/lib/i18n/en.ts +++ b/apps/web/src/lib/i18n/en.ts @@ -157,6 +157,7 @@ export const en: Catalog = { badgeManualOpen: "manual open", badgeSubRefused: "subscription refused", badgeSubSale: "subscription sale", + badgeWindowCharge: "out-of-window — owes fee", badgeNoTicket: "ticket not printed", feedSourceBooth: "booth", feedSourceReader: "reader", diff --git a/apps/web/src/lib/i18n/sq.ts b/apps/web/src/lib/i18n/sq.ts index 31f30e3..2e90d6a 100644 --- a/apps/web/src/lib/i18n/sq.ts +++ b/apps/web/src/lib/i18n/sq.ts @@ -161,6 +161,7 @@ export const sq = { badgeManualOpen: "hapje manuale", badgeSubRefused: "abonimi u refuzua", badgeSubSale: "shitje abonimi", + badgeWindowCharge: "jashtë orarit — detyrim", badgeNoTicket: "bileta nuk u printua", feedSourceBooth: "kabinë", feedSourceReader: "lexues", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8e122e8..84529da 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -287,6 +287,15 @@ export interface LedgerPayload { /** cash_in / cash_out voucher: a human-facing voucher number printed on the slip * (Mandat Nr.). Sequential per type; signed for reproducibility. */ readonly voucherNo?: string; + /** subscription tariff-bridge: an early-entry / late-exit transient charge OWED for + * parking outside the plan's allowed window, stamped on the vehicle_entry and collected + * (gated) at exit. The priced gap + tariff version travel alongside for reproducibility. + * See wiki/entities/subscription.md ("tariff bridge"). */ + readonly windowOwedMinor?: number; + readonly windowCurrency?: string; + readonly windowTariffVersionId?: string; + readonly windowGapStart?: string; + readonly windowGapEnd?: string; /** Free-form for forward-compat without a schema change. */ readonly [k: string]: unknown; }