fix: loop-A fresh review (503c326 baseline) — overflow-guard bypass, discounted-deposit retry, GDPR audit scrub, till cap, sweep rescue, 2FA reissue + SCA retry, consolidation round
Loop A fresh money/security/dup-mod review of the whole payments overhaul. 28 consolidated findings fixed:
MONEY:
- HIGH-1: B12 overflow guard now uses the discounted obligation — a pre-start deposit can never mint an unintended tip; the discount is never truncated to £0 when the customer pays the discounted deposit
- HIGH-2: discounted-deposit pending-reuse retry compares pendingStoredAmountPence vs chargeAmount (the actual Square amount), not req.Amount — no more permanent amount_mismatch 400 on lost-response retries
- MEDIUM-3: sweep rescue now carves overflow as a tip record + runs completion side-effects (was booking overflow as service revenue, skipping completion)
- MEDIUM-4 (shared w/ security): admin_audit_log.admin_id made nullable + anonymize_user/delete_guest_user NULL it + scrub details.card_last4 — 2fa_fallback_charge PII no longer survives account deletion
- MEDIUM-5: till gift-card payment now passes the £5,000/day admin cap (giftcard_limits)
- LOW-6: expired gift-card balance surfaced as expired/zero in GetUserGiftCardBalance
SECURITY:
- 2FA single-use consume made atomic at verify time for all 5 saved-card gates (fresh charges consume; pending-reuse retries don't); deferred consumption removed
- reissueTwoFACodeAfterFailedCharge routed through the fail-closed issuance gate (pepper check, cooldown) + fresh-only semantics (only when a code was actually consumed)
- family-alive cache invalidated on the stale-family cleanup DELETE (no 30s warm window after expiry)
- frontend 503-retry no longer reuses a consumed 2FA code — aligns with backend re-issue
DUP/MOD:
- reissue helper single-sourced (5 call sites), squareRefundStatusToLocal (10 inline switches), writeChargeSnapshot (7 sites, immutability guard on gift-card/till), postChargeRecheck (3+1 sites), scanIdempotencySlot (2), applyVATToChargeRecord (3 patterns), user_saved_cards upsert (2), BuyGiftCard pending INSERT via service
- till completed-dedup now re-validates paymentHasLiveRefund (aligns with booking/tip/gift-card)
- frontend 402 idempotency-key regeneration added to PaymentModal (aligns with other CIT surfaces)
- PAYMENT_METHOD_SAVED_CARD constant standardised ('saved_card' everywhere)
- admin audit coverage added for AdminRefundBooking + gift-card buy/top-up
- audit-helper cross-package dedup (user/twofa.go now calls payments' exported insert)
Verified: 26/26 dev + 24/24 prod packages, both vet tags, frontend tests + build, gitleaks clean.
This commit is contained in:
@@ -417,16 +417,17 @@ func Check(ctx context.Context, userID string, st *AttemptState, reqCode string,
|
||||
return OK, nil
|
||||
}
|
||||
|
||||
// ConsumePendingCode NULLs the user's pending 2FA code digest and expiry. The
|
||||
// payments saved-card charge gate verifies WITHOUT consuming (MEDIUM-2) and the
|
||||
// handlers call this when the charge reaches a TERMINAL SUCCESS state — inside
|
||||
// the transaction that records the completed charge when one exists — so the
|
||||
// code is consumed atomically with the charge OUTCOME, not the gate. A failed
|
||||
// or ambiguous Square charge leaves the code intact and the same-key retry can
|
||||
// re-verify the SAME code. Idempotent: consuming an already-NULL pending code
|
||||
// is a no-op, so a code still authorizes exactly one completed charge and can
|
||||
// never authorize a second after success. Accepts a db.Querier so the write can
|
||||
// ride the caller's transaction (pgx.Tx) or the pool proxy.
|
||||
// ConsumePendingCode NULLs the user's pending 2FA code digest and expiry.
|
||||
// Since finding 1 the saved-card CHARGE gates consume a FRESH charge's code at
|
||||
// verify time (consume=true — single-use), so this is no longer the gate's
|
||||
// consumption path: it is used by the PENDING-REUSE retry path, whose gate
|
||||
// verified WITHOUT consuming (consume=false) so a retry that fails again keeps
|
||||
// its code for one more attempt — the handlers call this when the retry reaches
|
||||
// a TERMINAL SUCCESS state, inside the transaction that records the completed
|
||||
// charge. Idempotent: consuming an already-NULL pending code is a no-op, so a
|
||||
// code still authorizes exactly one completed charge and can never authorize a
|
||||
// second after success. Accepts a db.Querier so the write can ride the caller's
|
||||
// transaction (pgx.Tx) or the pool proxy.
|
||||
func ConsumePendingCode(ctx context.Context, q db.Querier, userID string) error {
|
||||
if userID == "" {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user