feat(subs): scannable out-of-window slip + two-step booth flow

The advisory out-of-window slip for a subscriber had two problems:

1. Faulty character codes. It rendered via the generic text printReport,
   which has no CP852 mapping for the em dash, ellipsis, or warning sign in
   the composed strings — so they printed as "?" ("PARKIM ? JASHTE ORARIT").
   Added ASCII transliterations for that typographic punctuation in the
   ESC/POS encoder (— → -, ⚠ → !, … → ..., curly quotes/bullet), so they
   degrade to a readable glyph instead of "?".

2. Not scannable. The slip printed only "Nr: SUBSESS-…" as plain text, so
   the operator had to hand-key it. Gave the notice its own render function
   (renderWindowChargeNotice) + a printWindowChargeNotice device method that
   prints the occurrence id as a Code128 AND a QR — the same scan path as a
   transient ticket, so the operator scans it straight into the booth pay
   modal, which then quotes the combined window charge. Implemented on both
   the rongta and cashino drivers.

Also fixed the booth pay modal: "Open barrier" no longer shows by default
for a subscriber. A prepaid subscriber with nothing owed sees only a small
"assist open" reveal (the audited manual open for a faulty reader / lost
card stays available, just not the default). A subscriber owing an
out-of-window charge is now two steps — take payment first, then "Open
barrier" appears — instead of an always-on open button.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-21 13:12:05 +02:00
parent 0cbae94842
commit df5caf8d87
8 changed files with 224 additions and 64 deletions
+22
View File
@@ -247,6 +247,26 @@ export interface SubscriptionCardData {
readonly header?: TicketHeader;
}
/** An ADVISORY "out-of-window" slip for a subscriber who entered/exited outside
* their plan's allowed hours. NOT a payable ticket and carries NO final amount —
* the total is computed at the booth on settlement. It carries the OCCURRENCE id as
* a SCANNABLE Code128 + QR so the operator scans it straight into the booth pay
* modal (which then quotes the window charge) instead of hand-keying it — the same
* scan path as a transient ticket. See wiki/entities/subscription.md. */
export interface WindowChargeNoticeData {
/** The occurrence id (e.g. "SUBSESS-…") — the session identity the booth pay
* modal looks up. Encoded as the scannable code. */
readonly occurrenceId: string;
readonly holderName?: string | null;
/** When the scan happened (ISO-8601), printed as the human stamp. */
readonly at: string;
/** Entry (early) vs exit (late) — selects the wording. */
readonly edge: "entry" | "exit";
/** Minutes-from-midnight the allowed window opens, when known (entry slips). */
readonly windowOpensMin?: number | null;
readonly header?: TicketHeader;
}
export interface PrinterDevice extends Device {
printTicket(data: TicketData): Promise<void>;
/** Print a free-form text report (a shift Z-report, a receipt). `lines` are
@@ -259,6 +279,8 @@ export interface PrinterDevice extends Device {
* voucher mode it also carries the ticket-id barcode + grace window so it
* doubles as the self-exit voucher. See ReceiptData. */
printReceipt(data: ReceiptData): Promise<void>;
/** Print the advisory out-of-window slip with a scannable occurrence-id code. */
printWindowChargeNotice(data: WindowChargeNoticeData): Promise<void>;
}
export interface PrintReport {