fix: round-3 — tip gate asymmetry, webhook VAT align + 503 notifications, cash-tip campaign overcharge, lockout DoS, erasure durability, S3 retry cap, env parsing, per-user rate limiters, consume dead code, frontend 2FA remnants
- tip gate: CreateTipPayment saved-card 2FA gate now has scaTokenizedSavedCard skip matching every other charge surface (booking, terminal, gift-card); isSCATokenizeResultShape escape added to tip SAVE gate - webhook: align UPDATE clears VAT fields before re-apply (matches sweep rescue); 503 unknown-event tracking with 24h timeout notification via square_webhook_events table - cash-tip: cashChargeBasePence no longer restores campaign or subtracts loyalty — overcharge and tip shortfall fixed; 2FA dead code remnants removed from gift-card buy flow; TwoFactorCodeInput help text deconfused; refund pre-fill unit mismatch fixed (pounds vs pence); SCA buyer names split from full_name; passwordless delete UI accepts empty password - lockout: successful current-password clears shared failed_attempts/locked_until (victim can recover from login lockout via password change); passwordless delete condition changed to require 2FA only in enforced env - erasure: stale-guest batch erasure persists Square card/customer targets to durable outbox before NULLing them (crash-safe); S3 deletion retry capped at 10 attempts with admin notification; S3_PROFILE_PICS_BUCKET startup check added - env parsing: IsExplicitDevOrMockEnv and Square HTTP client base-URL switch now normalize (ToLower+TrimSpace) for consistency - auth: change-password/delete-account get per-user rate limiters (10/min); consume param dead code suppressed with TODO - frontend: 2FA/SCA dead code removed from gift-card buy flow, TwoFactorCodeInput help text fixed, refund pre-fill unit mismatch fixed, buyer names populated from full_name Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
@@ -404,10 +404,84 @@ func TestBuyGiftCard_ForeignIdempotencyKey_NotReused(t *testing.T) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// H4 — a COMPLETED provisional terminal checkout must be recorded, not just
|
||||
// released
|
||||
// Fix 1 — tip 2FA gate asymmetry: saved-card tip with SCA tokenize-result
|
||||
// must skip the gate in an enforced deployment
|
||||
// =============================================================================
|
||||
|
||||
// TestCreateTipPayment_EnforcedSavedCard_SCATokenizeResult_Succeeds locks the
|
||||
// Fix 1 gate skip: a saved-card tip carrying an SCA tokenize-result token
|
||||
// (new_card_token alongside card_id) must skip the 2FA gate and complete,
|
||||
// matching the CreateBookingPayment scaTokenizedSavedCard pattern. Without the
|
||||
// fix, the tip path gates on card_id alone and refuses 402
|
||||
// verification_required because the legacy verification_token field is empty.
|
||||
func TestCreateTipPayment_EnforcedSavedCard_SCATokenizeResult_Succeeds(t *testing.T) {
|
||||
helperEnvEnforce2FAStaging(t)
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, bookingID, _ := setupTestDataPast(t, ctx, tx)
|
||||
userToken := jwt.GenerateUserToken(userID)
|
||||
// A completed payment is required before a tip can be added.
|
||||
_, err := fixtures.CreateTestPayment(tx, bookingID, 5000.00, "online_square", "full", "completed")
|
||||
require.NoError(t, err)
|
||||
cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "ccof:mock_tip_sca_ok", "VISA", "4242")
|
||||
require.NoError(t, err)
|
||||
|
||||
origClient := SquareClient
|
||||
mc := square.NewDevClient().(*square.MockClient)
|
||||
mc.SimulateSavedCardVerificationRequired = true
|
||||
SquareClient = mc
|
||||
defer func() { SquareClient = origClient }()
|
||||
|
||||
scaToken := "cnon:sca-4242_500_ok"
|
||||
req := CreateTipPaymentRequest{
|
||||
Amount: 500,
|
||||
CardID: &cardID,
|
||||
NewCardToken: &scaToken,
|
||||
IdempotencyKey: "enforced-tip-scatokenized",
|
||||
}
|
||||
|
||||
w := makePaymentRequest(withNonGuest(CreateTipPayment), "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, ctx)
|
||||
require.Equal(t, http.StatusOK, w.Code, "an SCA tokenize-result tip must skip the enforced gate and complete, body: %s", w.Body.String())
|
||||
|
||||
var payCount int
|
||||
require.NoError(t, tx.QueryRow(ctx, `SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_type = 'tip'`, bookingID).Scan(&payCount))
|
||||
require.Equal(t, 1, payCount, "the SCA-tokenized tip must record exactly one completed tip payment")
|
||||
}
|
||||
|
||||
// TestCreateTipPayment_EnforcedSavedCard_SCATokenizeResult_SaveCard_Succeeds
|
||||
// locks the Fix 1 SAVE gate skip: a save-card tip carrying an SCA tokenize-result
|
||||
// token must skip the SAVE gate and persist the card, matching the
|
||||
// CreateBookingPayment isSCATokenizeResultShape pattern.
|
||||
func TestCreateTipPayment_EnforcedSavedCard_SCATokenizeResult_SaveCard_Succeeds(t *testing.T) {
|
||||
helperEnvEnforce2FAStaging(t)
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, bookingID, _ := setupTestDataPast(t, ctx, tx)
|
||||
userToken := jwt.GenerateUserToken(userID)
|
||||
_, err := fixtures.CreateTestPayment(tx, bookingID, 5000.00, "online_square", "full", "completed")
|
||||
require.NoError(t, err)
|
||||
|
||||
// A cnon:sca-... token with save_card=true and no card_id is the
|
||||
// NEW-card SCA tokenize-result save shape — isSCATokenizeResultShape
|
||||
// must recognise it and skip the SAVE gate. Use the regular mock
|
||||
// (no SimulateSavedCardVerificationRequired) so the ccof charge from
|
||||
// CreateCardOnFile succeeds.
|
||||
scaToken := "cnon:sca-round9-save-tip"
|
||||
req := CreateTipPaymentRequest{
|
||||
Amount: 500,
|
||||
NewCardToken: &scaToken,
|
||||
SaveCard: true,
|
||||
IdempotencyKey: "enforced-tip-scasave",
|
||||
}
|
||||
|
||||
w := makePaymentRequest(withNonGuest(CreateTipPayment), "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, ctx)
|
||||
require.Equal(t, http.StatusOK, w.Code, "an SCA tokenize-result tip with save_card=true must skip the enforced SAVE gate and complete, body: %s", w.Body.String())
|
||||
|
||||
var payCount int
|
||||
require.NoError(t, tx.QueryRow(ctx, `SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_type = 'tip'`, bookingID).Scan(&payCount))
|
||||
require.Equal(t, 1, payCount, "the SCA-tokenized save-card tip must record exactly one completed tip payment")
|
||||
}
|
||||
|
||||
// TestActiveTerminalCheckoutID_ProvisionalCompleted_RecordsPayment locks the
|
||||
// H4 fix: when activeTerminalCheckoutID discovers a provisional (tmp-)
|
||||
// checkout COMPLETED at Square, it must RECORD the payment (mirroring the
|
||||
|
||||
Reference in New Issue
Block a user