fix: dup/mod secondary round — till 2FA gate parity, mint-cooldown single source, notification parity, pence comments

Loop B dup/mod attack findings:
- TillPurchases admin 2FA gate now mirrors PaymentModal and the backend: gateActive = twoFactorEnforced && customerTwoFactorEnabled && paymentMethod='saved_card'; the customer's setup flag is fetched from GET /api/admin/users/{id} on selection. A 2FA-disabled customer in an enforced env no longer hits a dead-end blocked input — the charge 403 surfaces the actionable message via the existing self-heal.
- Extracted twoFAMintThrottled helper shared by SetupTwoFAHandler and ensurePendingTwoFACode — mint-cooldown rule can no longer drift between setup and disable-flow paths
- Notification-helper drift documented: sweep copy states the intentional booking+user scoping vs the canonical webhook copy (cross-referenced); auth refresh_token_reuse insert verified to carry the same NOT EXISTS acknowledged_at IS NULL guard; no import cycle (webhooks→payments one-way)
- Pence convention: 'rounded to the cent' corrected to 'pence' (handlers.go:2726)

26/26 backend packages; 72/72 frontend tests + build; env-docs 41/41.
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 7c424b28b8
commit b46927336b
5 changed files with 74 additions and 22 deletions
+4 -2
View File
@@ -667,8 +667,10 @@ func disputeNotificationID(squareDisputeID string) string {
// an.booking_id IS NOT DISTINCT FROM $1 AND an.acknowledged_at IS NULL` (a
// notification blocks a re-notify until the admin acknowledges it, then a NEW
// event re-arms). sweep.go's insertCriticalPaymentNotification mirrors this
// predicate exactly (its copy is the same guard over (reason, booking_id,
// user_id)); if the guard ever changes, sweep.go must be updated to match.
// predicate in shape (its copy applies the same NOT EXISTS/acknowledged_at
// IS NULL guard over (reason, booking_id, user_id) — an intentional
// user-scoping addition for sweep-originated events, documented on the sweep
// copy itself); if the guard ever changes, sweep.go must be updated to match.
//
// Untracked disputes (no local payment row, booking_id NULL) pass disputeID
// instead: each DISTINCT square dispute gets its OWN notification under the