//go:build test && dev package payments import ( "encoding/json" "net/http" "testing" "crussell/internal/square" "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" "github.com/stretchr/testify/require" ) // ============================================================================= // MEDIUM-HIGH: dev mock SCA error-shape parity at the HANDLER level. // // square_dev.go's SimulateSavedCardVerificationRequired is exercised heavily in // internal/square's own suite, but the frontend only ever sees the MOCK's SCA // rejection through a payment HANDLER — the 402 + {"code":"verification_required"} // body is what the SCA challenge flow keys on. These tests pin that the mock's // simulated SCA rejection surfaces the structured body end to end, and that the // ENFORCED-deployment gate refuses token-less saved-card charges while genuine // SCA tokenize-results (new_card_token = cnon:sca-...) sail through. // ============================================================================= // TestBookingPayment_MockSavedCardSCA_Tokenless_402_VerificationRequired drives // the mock's SimulateSavedCardVerificationRequired toggle through // CreateBookingPayment: a token-less saved-card (ccof) charge is rejected by // the mock with CARD_DECLINED_VERIFICATION_REQUIRED, and the handler must // surface it as 402 + the structured verification_required body — the exact // shape the frontend's isVerificationRequiredSignal parses to trigger the // client-side challenge. func TestBookingPayment_MockSavedCardSCA_Tokenless_402_VerificationRequired(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, bookingID, _ := setupTestData(t, ctx, tx) userToken := jwt.GenerateUserToken(userID) cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "ccof:mock_sca_booking", "VISA", "4242") require.NoError(t, err) origClient := SquareClient mc := square.NewDevClient().(*square.MockClient) mc.SimulateSavedCardVerificationRequired = true SquareClient = mc defer func() { SquareClient = origClient }() req := CreateBookingPaymentRequest{ Amount: 2500, PaymentType: "deposit", CardID: &cardID, IdempotencyKey: "mock-sca-booking-tokenless", } w := makePaymentRequest(withNonGuest(CreateBookingPayment), "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx) assertStructuredVerificationRequired(t, w) // A refused charge must not record a completed payment. var payCount int require.NoError(t, tx.QueryRow(ctx, `SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed'`, bookingID).Scan(&payCount)) require.Zero(t, payCount, "a mock-SCA-refused saved-card charge must not record a completed payment") } // TestBookingPayment_MockSavedCardSCA_SCATokenizeResult_Succeeds proves the // other half of the mock parity: under the SAME SimulateSavedCardVerificationRequired // toggle, a saved-card charge carrying a GENUINE tokenize-result // (new_card_token = cnon:sca-... — card.tokenize(verificationDetails, cardId)) // is accepted by the mock (the token IS the buyer verification) and the handler // completes the charge. func TestBookingPayment_MockSavedCardSCA_SCATokenizeResult_Succeeds(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, bookingID, _ := setupTestData(t, ctx, tx) userToken := jwt.GenerateUserToken(userID) cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "ccof:mock_sca_booking_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_2500_ok" req := CreateBookingPaymentRequest{ Amount: 2500, PaymentType: "deposit", CardID: &cardID, NewCardToken: &scaToken, IdempotencyKey: "mock-sca-booking-tokenized", } w := makePaymentRequest(withNonGuest(CreateBookingPayment), "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx) require.Equal(t, http.StatusOK, w.Code, "a genuine SCA tokenize-result must complete the saved-card charge, body: %s", w.Body.String()) } // TestBookingPayment_EnforcedSavedCard_Tokenless_402 pins the SCA-only gate on // the booking surface in an ENFORCED deployment: a token-less saved-card charge // is refused 402 verification_required up front (PSR 2017 reg 100 — no homegrown // 2FA fallback), before any charge reaches Square. func TestBookingPayment_EnforcedSavedCard_Tokenless_402(t *testing.T) { helperEnvEnforce2FAStaging(t) ctx, tx := testutils.SetupTestTx(t) userID, bookingID, _ := setupTestData(t, ctx, tx) userToken := jwt.GenerateUserToken(userID) cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "ccof:mock_enforced_booking", "VISA", "4242") require.NoError(t, err) req := CreateBookingPaymentRequest{ Amount: 2500, PaymentType: "deposit", CardID: &cardID, IdempotencyKey: "enforced-booking-tokenless", } w := makePaymentRequest(withNonGuest(CreateBookingPayment), "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx) assertStructuredVerificationRequired(t, w) var body map[string]string require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) require.Equal(t, "verification_required", body["code"]) } // TestBookingPayment_EnforcedSavedCard_SCATokenizeResult_Succeeds proves the // SCA-primary skip in an ENFORCED deployment: a saved-card charge carrying a // genuine SCA tokenize-result (new_card_token = cnon:sca-...) skips BOTH 2FA // gates (scaTokenizedSavedCard) and completes — the wire contract the frontend // sends after a successful tokenizeWithVerification challenge. func TestBookingPayment_EnforcedSavedCard_SCATokenizeResult_Succeeds(t *testing.T) { helperEnvEnforce2FAStaging(t) ctx, tx := testutils.SetupTestTx(t) userID, bookingID, _ := setupTestData(t, ctx, tx) userToken := jwt.GenerateUserToken(userID) cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "ccof:mock_enforced_booking_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_2500_ok" req := CreateBookingPaymentRequest{ Amount: 2500, PaymentType: "deposit", CardID: &cardID, NewCardToken: &scaToken, IdempotencyKey: "enforced-booking-tokenized", } w := makePaymentRequest(withNonGuest(CreateBookingPayment), "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx) require.Equal(t, http.StatusOK, w.Code, "an SCA tokenize-result 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'`, bookingID).Scan(&payCount)) require.Equal(t, 1, payCount, "the SCA-tokenized charge must record exactly one completed payment") }