Money-safety idempotency hardening (I1, wide):
- validate:"max=45" on CreateTerminalPayment/BookingPayment/Refund/Tip/
BuyGiftCard idempotency keys (all feed Square's 45-char /v2/payments,
/v2/refunds, /v2/cards caps); BuyGiftCard corrected from a wrongly-loose
max=64. Till keeps max=64 (its key also feeds the 64-char terminal-checkout
endpoint).
- Explicit 45-char guard in RefundPayment: the one handler that decodes
RefundRequest without running the struct validator, so the tag alone was
inert; a longer key would 400 at Square and be misclassified as a
definitive refund decline.
- New TestIdempotencyKey_OverLength_RejectedAcrossPaymentHandlers covers all
six endpoints (terminal saved-card, booking, tip, gift-card, till, refund).
Stable-sentinel card identity in idempotency keys (C1, wide):
- BookingFlow deposit key now uses the 'new-card' sentinel instead of
embedding the cnon: nonce (matches UserPaymentModal/account). A re-tokenize
after a spent nonce no longer regenerates the key, closing a lost-response
double-charge window.
- TipPayment + UserBookingModal tip keys now include card identity
(selectedCardId || 'new-card'); previously keyed on amount only, so a
same-amount tip on a DIFFERENT card reused the key and deduped a distinct
charge. Resets cleared in every success/close path.
Test isolation (R1): TestRefund_PendingResume_NewKeyAfterModalReopen no
longer t.Parallel — it swaps the package-level SquareClient mid-test and a
concurrent parallel test could observe the swapped instance.
Naming/quality (M1/M2/M4): resolveChargeSource local renamed savedRowID (was
shadowing the cardID *string parameter); BuyGiftCard fallback prefix
"till-" -> "gc-"; saved-card terminal response key "checkout_id" -> "payment_id"
(it holds a DB payment row, not a Square checkout) with matching frontend
fallback. README maintenance-job count corrected 24 -> 25.
Full suite 25/25 + race clean via run-tests.sh lockfile; svelte-check 0
errors/warnings; production build succeeds.