Money-safety idempotency fixes (external review bugs 1-3):
- processChargeGroup: aggregated refund key now hashes the sorted pending-row
set (chargeID-square-agg-<sha256 suffix>) so a changed group can never mark
a new row completed against an old smaller refund; >45-char chargeIDs use a
hashed prefix instead of verbatim truncation (which would collide charges on
Square's global key dedup). Same-set crash-retry keeps Square's dedup.
- CreateTerminalPayment saved_card: two-tier idempotency key — client-supplied
per-attempt UUID preferred (distinct identical charges no longer collapse),
deterministic booking+type+amount+card fallback for no-key retry safety.
PaymentModal sends a per-charge UUID cleared after success.
- ensureRefundKey: legacy NULL-key manual refunds persist a generated key to
the row BEFORE the Square call (race-safe AND idempotency_key IS NULL guard),
so a lost-response retry reuses the key and never double-refunds. Wired into
resumeManualPendingRefund and the sweep's manual-retry loop.
Classification + money-safety hardening:
- till.go/sweep.go: structured square.ErrorCode/IsNotFound are authoritative
when present; message-substring matching only for non-structured errors
(dev mock, client-side status errors). Fixes fragile string-matching driving
sweep retries and gift-card clawbacks.
- SaveCardForUser: ON CONFLICT (user_id, square_card_id) DO NOTHING + re-select
(was a latent UNIQUE-violation 500 on save-card retry).
- CreateBookingPayment: partial payments re-validated against remaining balance
inside the advisory lock (closes concurrent-overpayment race).
- InvalidateSquareCustomerCache on GDPR erasure paths (account.go,
time-blockers.go stale-guest anonymization).
- GetUserGiftCardBalanceAdmin: in-handler admin check (defense-in-depth).
- getCheckoutHTTP: warn on multi-payment checkouts instead of dropping
payments[1:].
- Cash/giftcard terminal branch: removed dead idempotency SELECT, "tip-" ->
"till-" prefix.
- UserPaymentModal: removed vestigial polling state; proper interval cleanup.
- account/+page.svelte: gift-card redeem dialog links /terms.
- nginx CSP: allow *.squarecdn.com and js.squareup.com so the Square Web
Payments SDK + card iframe can tokenize behind the proxy.
Tests: +8 regression tests covering changed-set refund keys, legacy NULL-key
single-refund, saved-card client-key dedup/no-dedup, concurrent partials, and
cache invalidation. Full suite + race detector clean via run-tests.sh lockfile.