DEV Community

Tudor Brad
Tudor Brad

Posted on • Originally published at betterqa.co

Payment testing: the card types that break in production

The bug that costs you money twice

Last year we tested a fintech client's checkout flow. Everything passed in Stripe test mode. Green across the board. Then they went live in Germany and 30% of transactions started failing silently. No error page. No retry prompt. Just... nothing happened when the user clicked "Pay."

The problem was 3D Secure. Their integration handled the initial charge request fine, but never implemented the redirect flow for SCA (Strong Customer Authentication). In test mode, Stripe skips 3D Secure unless you explicitly use the 4000002760003184 test card. Nobody on the dev team had used that card. So nobody knew the integration was broken for every European card that required authentication.

The client found out when chargebacks started hitting. That is the worst way to discover a payment bug: your payment processor tells you, your bank tells you, and your users have already left.

Why payment bugs are different from other bugs

A broken image on your landing page is embarrassing. A broken payment flow is expensive. Here is what makes payment bugs uniquely painful:

Direct revenue loss. Every failed transaction is money that almost entered your account and didn't. If 5% of your transactions fail due to a card type you never tested, that is 5% of revenue gone. Not "at risk." Gone.

Chargebacks compound the damage. When a payment goes through incorrectly (wrong amount, duplicate charge, currency mismatch), you don't just refund the money. You pay chargeback fees. Enough chargebacks and your payment processor raises your rates or drops you entirely.

User trust evaporates instantly. People are anxious about money. A single failed payment makes a user question whether your site is legitimate. They won't debug it for you. They will close the tab and buy from someone else.

Silent failures hide the problem. Unlike a 500 error that shows up in your monitoring, many payment failures happen at the processor level and return a generic decline. Your logs show "card_declined" but the real cause is that your integration doesn't handle the card network correctly.

This is why we treat payment testing as its own discipline, not just "form validation with a credit card field."

Card types that actually break things

Here are the specific card type issues we run into repeatedly when testing payment integrations for clients.

Amex and the 15-digit problem

American Express cards have 15 digits and a 4-digit CVV (called CID). Visa and Mastercard have 16 digits and a 3-digit CVV. This sounds trivial until you see how many integrations hardcode maxLength="16" on the card number input and maxLength="3" on the CVV field.

We tested a SaaS platform where Amex cards were being silently rejected. No error message. The form just wouldn't submit. The frontend validation required exactly 16 digits, so any 15-digit PAN was treated as incomplete. The user saw a disabled submit button and assumed they typed something wrong.

Test cards to use:

Amex:           3782 822463 10005    (15 digits, 4-digit CID)
Visa:           4242 4242 4242 4242  (16 digits, 3-digit CVV)
Mastercard:     5555 5555 5555 4444  (16 digits, 3-digit CVV)
Enter fullscreen mode Exit fullscreen mode

What to check:

  • Card number field accepts 15, 16, and 19 digits
  • CVV field accepts both 3 and 4 digits
  • Card type detection updates dynamically (Amex logo appears when you type 37xx)
  • Backend validation matches frontend rules

UnionPay and 19-digit PANs

UnionPay cards can be 16, 17, 18, or 19 digits long. If your validation regex is ^\d{16}$, you are rejecting a card network used by over a billion people.

We see this constantly in integrations targeting Asian markets. The dev team builds and tests with Visa/Mastercard, launches in Singapore or Malaysia, and gets support tickets from users who "can't enter their card number."

UnionPay (19):  6200 0000 0000 0000 003
UnionPay (16):  6200 0000 0000 0005
Enter fullscreen mode Exit fullscreen mode

The fix is straightforward: accept 13-19 digits and let the payment processor handle network-specific validation. Your frontend should not be the gatekeeper for PAN length.

Diners Club and the 14-digit edge case

Diners Club cards traditionally have 14 digits, though newer ones may have 16. If your system strips spaces and then checks length === 16, Diners Club users cannot pay.

Diners Club:    3056 9309 0259 04   (14 digits)
Enter fullscreen mode Exit fullscreen mode

This one is less common globally but still matters if you operate in parts of South America or accept corporate cards. We have seen it break on subscription billing platforms where the initial charge worked (the card was tokenized by Stripe directly) but a later recurring charge failed because the platform's own validation ran during a card update flow.

3D Secure and SCA failures

This is the big one. 3D Secure (3DS) adds an authentication step where the card issuer verifies the cardholder, usually through a redirect or iframe popup. In the EU, SCA regulations make this mandatory for most online transactions.

The problem: Stripe's test mode does not trigger 3DS by default. You need to explicitly use test cards that simulate the 3DS flow:

3DS required:       4000 0027 6000 3184
3DS required (fail): 4000 0084 0000 1629
3DS optional:       4000 0025 0000 3155
Enter fullscreen mode Exit fullscreen mode

What breaks:

  • The redirect URL is not configured, so the user gets sent to a blank page
  • The return handler does not check payment_intent.status after the redirect
  • Mobile webviews block the 3DS popup, so the authentication never completes
  • The webhook handler does not account for the requires_action status

We tested a client's mobile app where 3DS worked perfectly in the browser but failed 100% of the time in the iOS webview. The app's WKWebView had javaScriptEnabled set to true but blocked popups, which is how the 3DS challenge was presented. Every EU user on iOS could not complete a payment.

Currency and amount edge cases

Currency bugs are sneaky because they often produce a valid charge for the wrong amount. The user gets billed, the amount looks plausible, and nobody notices until reconciliation.

Common issues we test for:

Zero-decimal currencies. JPY, KRW, and several others do not use decimal subunits. If your system sends 1000 to Stripe for a 10.00 USD charge (correct, because Stripe uses cents), sending 1000 for a JPY charge means 1000 yen, not 10 yen. The amount field interpretation changes by currency.

# USD: $10.00 = 1000 (cents)
# JPY: 1000 yen = 1000 (no subunit)
# BHD: 10.000 BD = 10000 (three decimal places)
Enter fullscreen mode Exit fullscreen mode

Rounding on conversion. If your platform shows prices in EUR but charges in USD after conversion, rounding differences can mean the user sees 9.99 EUR but gets charged 10.01 EUR equivalent. Small difference. Big trust problem.

Minimum charge amounts. Stripe requires a minimum of 50 cents USD (or equivalent). If your platform allows a 0.10 USD tip or a discount that reduces the charge below the minimum, the payment fails at the processor level with a generic error.

How we structure payment test suites

When we pick up a payment integration project, here is the sequence we follow. This is not theory. This is what we actually run.

Phase 1: Card type coverage matrix. We build a grid of every card network the client wants to support, crossed with every payment scenario (one-time charge, subscription, refund, partial refund, card update). Each cell gets tested. No assumptions that "if Visa works, Mastercard works."

Phase 2: Authentication flows. We test every 3DS path: success, failure, abandonment (user closes the popup), timeout, and network error during redirect. We test on desktop browsers, mobile browsers, and in-app webviews separately because they behave differently.

Phase 3: Error handling and messaging. We trigger every decline code Stripe can return (insufficient funds, expired card, incorrect CVV, processing error, card not supported) and verify the user sees a specific, actionable message. "Payment failed" is not acceptable. "Your card was declined. Please check your card details or try a different payment method" is the minimum.

Phase 4: Webhook reliability. We verify that payment confirmation does not depend solely on the client-side redirect. If the user closes their browser after 3DS but before the redirect completes, the webhook from Stripe should still update the order. We test this by intentionally killing the browser session mid-payment and confirming the backend processes the webhook correctly.

Phase 5: Currency and locale. We test with cards issued in different countries, in different currencies, with different locale settings on the browser. A Japanese user with a JPY card on a platform that prices in USD should see a coherent experience from price display through to their bank statement.

Stripe test cards quick reference

For developers setting up their own payment test suites, here are the cards we use most often:

Scenario Card number Notes
Success 4242 4242 4242 4242 Always succeeds
Generic decline 4000 0000 0000 0002 Always declined
Insufficient funds 4000 0000 0000 9995 Specific decline reason
Incorrect CVC 4000 0000 0000 0127 CVC check fails
Expired card 4000 0000 0000 0069 Expiry check fails
3DS required 4000 0027 6000 3184 Triggers authentication
3DS failure 4000 0084 0000 1629 Authentication fails
Amex 3782 822463 10005 15 digits, 4-digit CID
Dispute/chargeback 4000 0000 0000 0259 Triggers dispute

Use any future expiry date and any 3-digit CVC (4-digit for Amex). For full documentation, check Stripe's testing page.

The test mode trap

Here is the pattern we see over and over: a team builds a payment integration, tests it thoroughly in Stripe test mode, and ships it. Then production breaks in ways that test mode never revealed.

Test mode is not production. It does not enforce SCA. It does not check real BIN ranges. It does not apply real fraud detection rules. It does not connect to actual card networks. It is a simulation, and like all simulations, it has blind spots.

The gap between test mode and production is where payment bugs live. You can narrow that gap by using the right test cards, testing authentication flows explicitly, and verifying webhook handling under failure conditions. But you cannot eliminate it entirely without production monitoring.

We always recommend that clients set up real-time alerting on payment failure rates. A 2% failure rate on day one that creeps to 8% by day thirty means something changed at the processor or issuer level, and no amount of pre-launch testing catches that.

What we have learned from testing payments across clients

After testing payment integrations for fintech and e-commerce clients at BetterQA, a few things stand out:

  1. Card type validation belongs at the processor level, not your frontend. Let Stripe or Adyen validate the PAN. Your job is to not block valid cards before they reach the processor.

  2. 3D Secure is not optional in Europe. If you sell to EU customers and your integration does not handle 3DS, you will lose transactions. Not might. Will.

  3. Test the sad paths harder than the happy paths. A successful payment needs to work. A failed payment needs to communicate clearly. Most teams spend 90% of testing time on success and 10% on failure. We flip that ratio.

  4. Webhooks are your safety net. Client-side confirmation is unreliable. Browsers crash, users close tabs, networks drop. Your backend must handle payment confirmation through webhooks independently of what happens in the browser.

  5. Currency handling is a category of bugs, not a single check. Zero-decimal currencies, three-decimal currencies, conversion rounding, minimum amounts: each one is a distinct failure mode.

Payment bugs are expensive, embarrassing, and preventable. The card types and scenarios in this article are the ones we see break most often. Test them before your users find them for you.

More on how we approach QA for complex integrations: betterqa.co/blog

Top comments (0)