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:
@@ -1072,10 +1072,18 @@ BEGIN
|
||||
-- record (GDPR Art 30) that MUST survive erasure for audit retention, but
|
||||
-- rows like '2fa_fallback_charge' (insertTwoFAFallbackAudit) carry
|
||||
-- target_user_id = the erased user, so the user link is NULLed to keep the
|
||||
-- record de-identified. Mirrors delete_guest_user() exactly — only
|
||||
-- target_user_id is scrubbed, NOT details (delete_guest_user also leaves
|
||||
-- details untouched, so the two erasure paths stay consistent).
|
||||
-- record de-identified. admin_id is NULLed too: for a customer-initiated
|
||||
-- 2FA-fallback charge the ACTOR (admin_id) IS the erased customer, so the
|
||||
-- actor link must equally be scrubbed — and for delete_guest_user the NULL
|
||||
-- unblocks the users-row DELETE the FK would otherwise reject.
|
||||
-- card_last4 in the retained details (2fa_fallback_charge rows) is card
|
||||
-- payment data (PII): it is scrubbed FIRST (while the row still references
|
||||
-- the target) so no card identifier survives erasure.
|
||||
UPDATE admin_audit_log
|
||||
SET details = jsonb_set(details, '{card_last4}', 'null'::jsonb)
|
||||
WHERE target_user_id = target_id OR admin_id = target_id;
|
||||
UPDATE admin_audit_log SET target_user_id = NULL WHERE target_user_id = target_id;
|
||||
UPDATE admin_audit_log SET admin_id = NULL WHERE admin_id = target_id;
|
||||
|
||||
-- Clear login audit trail (no ongoing legal basis after account closure)
|
||||
DELETE FROM login_audit WHERE user_id = target_id;
|
||||
@@ -1172,7 +1180,17 @@ BEGIN
|
||||
UPDATE gift_cards SET redeemed_by = NULL WHERE redeemed_by = target_id;
|
||||
UPDATE gift_cards SET created_by = NULL WHERE created_by = target_id;
|
||||
UPDATE admin_notifications SET user_id = NULL WHERE user_id = target_id;
|
||||
-- admin_id may also reference the guest: a guest-initiated 2FA-fallback
|
||||
-- charge writes admin_id = the guest's own userID. The actor link is
|
||||
-- NULLed before the user row below is deleted (the FK would otherwise
|
||||
-- reject the DELETE), and the card PII in the retained details
|
||||
-- (card_last4) is scrubbed FIRST — while the row still references the
|
||||
-- guest — so no card identifier survives.
|
||||
UPDATE admin_audit_log
|
||||
SET details = jsonb_set(details, '{card_last4}', 'null'::jsonb)
|
||||
WHERE target_user_id = target_id OR admin_id = target_id;
|
||||
UPDATE admin_audit_log SET target_user_id = NULL WHERE target_user_id = target_id;
|
||||
UPDATE admin_audit_log SET admin_id = NULL WHERE admin_id = target_id;
|
||||
|
||||
DELETE FROM users WHERE id = target_id AND account_role = 'guest';
|
||||
END;
|
||||
@@ -2449,7 +2467,12 @@ CREATE INDEX idx_gift_card_transactions_created_at ON gift_card_transactions(cre
|
||||
|
||||
CREATE TABLE admin_audit_log (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_admin_audit_log_id(),
|
||||
admin_id CHAR(12) NOT NULL REFERENCES users(id),
|
||||
-- admin_id is nullable: a customer-initiated audit row (e.g. a 2FA-fallback
|
||||
-- charge where the CIT actor IS the customer) is de-identified by NULLing
|
||||
-- admin_id at erasure (anonymize_user / delete_guest_user), and a guest
|
||||
-- actor's row must be NULLed before delete_guest_user can delete the user
|
||||
-- (the FK would otherwise block the erasure).
|
||||
admin_id CHAR(12) REFERENCES users(id),
|
||||
action_type VARCHAR(30) NOT NULL,
|
||||
target_user_id CHAR(12) REFERENCES users(id),
|
||||
target_gift_card_id CHAR(12) REFERENCES gift_cards(id),
|
||||
|
||||
Reference in New Issue
Block a user