domain project / fintech

Fintech & Payments

Test strategies for payment processors, digital wallets, lending platforms, and open banking integrations.

Payment processingDigital walletOpen banking (PSD2)KYC & identity verificationTransaction monitoringRegulatory compliance

Domain Overview

Fintech applications move money — testing failures can mean financial loss, regulatory sanctions, or fraud. The testing challenge is the dependency on third-party payment rails (Stripe, Adyen, Plaid) and regulatory verification services (KYC/AML providers).

Typical stack: Node.js API, React dashboard, Stripe/Adyen/Braintree payment processing, Plaid open banking, Jumio/Onfido KYC, PostgreSQL for ledger.

Key Test Areas

AreaWhat to testRisk
Payment initiationCard, bank transfer, wallet — success and failure pathsDouble charge, failed payment
Wallet operationsDeposit, withdraw, balance, limitsWrong balance, blocked withdrawal
KYC flowDocument upload, identity check, rejection handlingCompliance violation
Open bankingBank connection, consent flow, account syncBroken sync, wrong data
Transaction historyAmounts, dates, statuses, currencyIncorrect statements
Limits & controlsDaily limits, velocity checks, fraud holdsFraud, regulatory breach

Test Pyramid for Fintech

Unit (55%):    Ledger arithmetic, FX conversion, limit rules, KYC state machine
Integration (35%): Payment API contracts (Stripe/Adyen test mode), webhook handling
E2E (10%):     Complete payment flow, wallet top-up, KYC submission

Webhooks are critical — payment status is often delivered asynchronously. Test webhook handling at the integration level.

Common Failure Scenarios

1. Webhook received before UI payment confirmation → race condition in status
   Test: intercept Stripe webhook, delay by 2s, verify UI handles pending state correctly

2. FX conversion rate used for display differs from rate used for charge
   Test: compare displayed rate in UI against actual charged amount in confirmation

3. KYC re-submission after rejection doesn't reset state → user stuck
   Test: submit invalid doc, verify rejection, re-submit valid doc → verify success path

4. Duplicate payment on network timeout retry
   Test: mock 408 response on first attempt, verify retry uses idempotency key, verify single charge

5. Wallet balance shows stale data after concurrent operations
   Test: two concurrent deposits via API, verify balance = initial + both amounts

Fintech-Specific Test Patterns

// Test Stripe payment with webhooks
test('payment webhook updates order status', async ({ page, request }) => {
  // Initiate payment
  await page.goto('/pay');
  await page.getByLabel('Amount').fill('50');
  await page.getByRole('button', { name: 'Pay' }).click();

  // Get payment intent ID from UI
  const intentId = await page.locator('[data-payment-intent]').getAttribute('data-payment-intent');

  // Simulate Stripe webhook (test environment only)
  await request.post('/api/webhooks/stripe', {
    data: {
      type: 'payment_intent.succeeded',
      data: { object: { id: intentId, status: 'succeeded', amount: 5000 } },
    },
    headers: { 'stripe-signature': process.env.STRIPE_WEBHOOK_SECRET! },
  });

  // Verify status updated
  await expect(page.getByRole('status')).toContainText('Payment confirmed');
});

// Verify idempotency
test('duplicate payment request with same key charges only once', async ({ request }) => {
  const idempotencyKey = `test-${Date.now()}`;
  const headers = { 'Idempotency-Key': idempotencyKey, Authorization: `Bearer ${process.env.TEST_TOKEN}` };
  const body = { amount: 1000, currency: 'USD', source: 'card_test' };

  const [res1, res2] = await Promise.all([
    request.post('/api/payments', { data: body, headers }),
    request.post('/api/payments', { data: body, headers }),
  ]);

  expect(res1.status()).toBe(200);
  expect(res2.status()).toBe(200);

  const p1 = await res1.json();
  const p2 = await res2.json();
  expect(p1.id).toBe(p2.id);  // same payment ID = idempotent
});