CAC Bank is an OTP debit with no redirect and no webhook: initiate SMSes a
code to the payer's mobile, and the charge only settles when that code is
confirmed. The payment service already spoke it (passenger uses it); the
freight side had the enum values but none of the flow.
API:
- PaymentClientService.confirmOtp forwards the code to
POST /payments/intents/:id/confirm, mapping 400/404 to BadRequest so a
mistyped code stays retryable instead of surfacing as a gateway failure.
- PaymentService.confirmOtp is keyed by the LOCAL intent id (the invoice's
paymentId) rather than the domain reference, so the right invoice settles
when several share a booking. On success billing settles the invoice.
- payInvoice rejects CAC_BANK without payerAccount before calling the
gateway, and no longer runs the demo auto-settle for a COLLECT_OTP intent
(it is not paid until the payer confirms).
- POST /billing/my-invoices/:id/confirm — ownership-checked, and since
warehouse fee invoices are central invoices it covers those too.
Portal:
- useInvoicePayment owns the whole flow (initiate, redirect-or-OTP, confirm)
and replaces the five near-identical pay mutations at the call sites.
- PaymentMethodModal gains the CAC Bank option, the payer mobile field, and
the OTP step. Click-outside is disabled there so a stray click cannot drop
the payer out of a live OTP window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the fixed 5-minute grace with a settlement check at expiry:
expire() calls the payment API's reconcile endpoint — paid intents are
kept and allocated via payment.succeeded, unverifiable results defer
expiry to the next tick, only verifiably unpaid holds expire.
The marshalling manifest cut off at one page ("... 10 more row(s) not
shown") because the fallback drew rows only until it hit the bottom band.
Now the table flows across as many pages as it needs:
- page 1 keeps the full header + summary tiles; continuation pages get a slim
"(continued — page N)" header and a re-drawn table header
- the verification notice and signature lines stay pinned to the final page,
moving to a fresh page when rows run too deep for the bottom band
- the copy watermark repeats on every page of its copy
- a 12-page safety cap keeps the old truncation note as a last resort
Verified by standalone render: 50-row manifest -> 2 pages, all 50 rows, no
truncation; 5-row doc stays 1 page; two-copy freight order still renders 2
watermarked pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildTabularFallbackPdf parsed the whole HTML at once, so a two-copy document
(freight order: Port Operations copy + Gate Security & Carrier copy) came out
as ONE page with every tile duplicated and no watermarks.
- documents wrapped in <section class="copy"> now render one page per copy,
each parsed independently (no more merged/duplicated tiles)
- each page carries its copy label as a large rotated light-gray watermark,
drawn beneath the content (new watermarkOp, 30-degree text matrix)
- new assemblePdf() multi-page assembler; assembleSinglePagePdf untouched for
its existing callers
- the meta label is parsed from the document ("Booking") instead of the
hardcoded "TRAIN / SCHEDULE"
Verified by compiling the util standalone and rendering a two-copy freight
order: 2 pages, both watermarks present, tiles once per page, valid xref/EOF;
copy-less documents (marshalling) still render a single unwatermarked page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>