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:
@@ -2723,7 +2723,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
|
||||
//
|
||||
// MONEY INVARIANT (deliberately kept exact): the returned records always
|
||||
// partition paymentAmount — deposit + balance + tip === paymentAmount exactly
|
||||
// (every component is rounded to the cent and the parts are derived from one
|
||||
// (every component is rounded to the pence and the parts are derived from one
|
||||
// another, so no rounding residue exists). The sum of the split records can
|
||||
// therefore never exceed the amount actually charged at Square. When deposit
|
||||
// AND balance are both zero (booking already fully paid) the tip record alone
|
||||
|
||||
@@ -1245,8 +1245,19 @@ func leaveGiftCardPurchasePending(ctx context.Context, r staleRow) {
|
||||
// ties to a user (gift-card purchases). The NOT EXISTS guard keeps ONE
|
||||
// notification per issue instead of one per sweep run, and requires the prior
|
||||
// notification to be unacknowledged (acknowledged_at IS NULL) so that after an
|
||||
// admin acknowledges it, a NEW event for the same booking/user re-notifies —
|
||||
// matching the webhook copy's guard (handlers/webhooks/square.go) exactly.
|
||||
// admin acknowledges it, a NEW event for the same booking/user re-notifies.
|
||||
//
|
||||
// This is the SWEEP-specific variant of a same-named helper in the webhooks
|
||||
// package (handlers/webhooks/square.go, insertCriticalPaymentNotification) with
|
||||
// an INTENTIONAL scoping difference: this one dedups on (reason, booking_id,
|
||||
// user_id) — booking and/or user — because sweep-originated events may be
|
||||
// user-attributed without a booking (gift-card purchases) or booking-attributed
|
||||
// without a user (untracked terminal charges). The webhook variant takes
|
||||
// (ctx, bookingID, disputeID string), never writes user_id, and dedups on
|
||||
// (reason, booking_id) or a deterministic per-dispute id. Both share the
|
||||
// admin_notifications table and the 'critical_payment_log' reason but serve
|
||||
// different call paths (sweep vs webhook) — do not merge them, and if the
|
||||
// NOT EXISTS guard shape ever changes, update BOTH copies.
|
||||
func insertCriticalPaymentNotification(ctx context.Context, bookingID, userID *string) {
|
||||
tag, err := db.Conn.Exec(ctx, `
|
||||
INSERT INTO admin_notifications (reason, booking_id, user_id, created_at)
|
||||
|
||||
Reference in New Issue
Block a user