Testland
Browse all skills & agents

refund-test-matrix-builder

Workflow-driven skill that builds a refund test matrix from a payment-flow inventory: refund variants (full / partial / multiple-partials / over-refund / refund-on-disputed / refund-on-already-refunded), per-gateway nuances (Stripe, Adyen, PayPal, Braintree refund APIs), and timing variants (immediate / next-day / declined-by-bank), one test case per cell. Use for refund coverage on a new integration; for chargeback / dispute coverage use chargeback-flow-test-author, for webhook redelivery + idempotency use payment-webhook-replay, and for the state model use payment-flow-states-reference.

Install with skills.sh (any agent)

npx skills add testland/qa --skill refund-test-matrix-builder
View source

refund-test-matrix-builder

Overview

Refund logic is the second-most-misimplemented part of payment flows (after 3DS). Common bugs: double-refund, refund-before- capture, partial-refund-not-summing, refund-of-already-refunded.

This skill walks through producing the refund test matrix - not exhaustive (every gateway × every variant × every timing = thousands), but covering the canonical cells.

When to use

  • New payment integration; need refund test coverage.
  • Refund-related incident; need to backfill tests.
  • Gateway migration; need to re-validate refund logic.

Step 1 - Inventory refund touchpoints

Grep for refund-issuing code:

grep -rn 'refund\|Refund\|REFUND' --include='*.{ts,js,py,java,go,rb,cs}' .

Categorise per gateway + per code path:

TouchpointGatewayTrigger
Order cancellation flowStripeUser clicks "cancel" within 24h
Customer service portalStripeCS rep issues partial refund
Subscription downgradeStripePro-rated refund for unused time
Cross-tenant chargeback handlerStripe / AdyenAutomatic on dispute lost

Step 2 - The 7 canonical refund test cases

For each (gateway, touchpoint):

#TestExpected
1Full refund of captured chargerefund.status = succeeded; charge.amount_refunded = charge.amount
2Partial refund (50%)refund.amount = 0.5x charge.amount; charge.amount_refunded reflects
3Multiple partial refunds summing to fullAll succeed; cumulative refunded = charge.amount; charge.refunded = true
4Over-refund attempt (101% of total)Gateway rejects; descriptive error
5Refund of already-fully-refunded chargeRejected with "charge_already_refunded" or equivalent
6Refund of failed chargeRejected; no refund created
7Refund of disputed chargePer gateway: blocks or allows but doesn't reverse dispute

Step 3 - Per-gateway refund patterns

Stripe

Per docs.stripe.com/refunds (opens in new window):

test('full refund', async () => {
  const intent = await createSucceededIntent({ amount: 1000 });
  const refund = await stripe.refunds.create({ payment_intent: intent.id });
  expect(refund.status).toBe('succeeded');
  expect(refund.amount).toBe(1000);
});

test('partial refund', async () => {
  const intent = await createSucceededIntent({ amount: 1000 });
  const refund = await stripe.refunds.create({ payment_intent: intent.id, amount: 500 });
  expect(refund.amount).toBe(500);
});

test('over-refund rejected', async () => {
  const intent = await createSucceededIntent({ amount: 1000 });
  await expect(
    stripe.refunds.create({ payment_intent: intent.id, amount: 1500 })
  ).rejects.toThrow(/refund_amount_exceeds_charge_amount/);
});

Adyen

Per docs.adyen.com/online-payments/refund (opens in new window):

const result = await checkout.modificationsCorrespondingRefund({
  originalReference: 'capture-pspReference',
  modificationAmount: { value: 500, currency: 'EUR' },
});
expect(result.response).toBe('[refund-received]');
// Actual refund completion via webhook (notification)

Adyen refunds are async; webhook handles [refund-received] [REFUND] settled.

PayPal

Per developer.paypal.com/docs/api/payments/v2#captures_refund (opens in new window):

const request = new paypal.payments.CapturesRefundRequest(captureId);
request.requestBody({ amount: { value: '5.00', currency_code: 'USD' } });
const result = await client.execute(request);
expect(result.result.status).toBe('COMPLETED');

Braintree

const result = await gateway.transaction.refund(transactionId, '5.00');
expect(result.success).toBe(true);
expect(result.transaction.type).toBe('credit');

Braintree requires settlement first per braintree-test-cards.

Step 4 - Timing variants

VariantTest
Immediate refundIssue + assert refund.status = succeeded
Async refund (Adyen, PayPal)Issue + poll webhook
Same-day (within auth window)Issue before settlement; assert auth-void semantics
Next-dayIssue after settlement; assert refund-credit semantics
Declined by bankPer gateway: simulated via specific failure-mode test card

Step 5 - Emit the test matrix

# tests/payment/refund-matrix.yaml
matrix:
  gateways:
    - stripe
    - adyen
    - paypal
    - braintree
  variants:
    - full
    - partial
    - multiple-partials
    - over-refund-attempt
    - already-refunded
    - failed-charge
    - disputed-charge
  timing:
    - immediate
    - async-webhook

For each (gateway, variant, timing) cell, generate a test:

// tests/payment/refund-stripe.test.ts
import { CASES } from './refund-matrix';

describe.each(CASES.stripe)('Stripe refund: $variant', ({ variant, expected }) => {
  test(variant, async () => {
    // ... per-variant logic + assertion
  });
});

Step 6 - Idempotency

Refunds are mutating operations; idempotency keys are critical:

test('idempotent refund', async () => {
  const intent = await createSucceededIntent({ amount: 1000 });
  const key = 'refund-' + intent.id;

  const r1 = await stripe.refunds.create({ payment_intent: intent.id, amount: 500 }, { idempotencyKey: key });
  const r2 = await stripe.refunds.create({ payment_intent: intent.id, amount: 500 }, { idempotencyKey: key });

  expect(r1.id).toBe(r2.id);
});

Without idempotency, network retries double-refund the customer.

Step 7 - Reporting + reconciliation

Refund-test coverage matrix should be reported per release:

## Refund Coverage Matrix

| Gateway | Variant | Immediate | Async Webhook |
|---|---|---|---|
| Stripe | full | ✅ | n/a (sync) |
| Stripe | partial | ✅ | n/a |
| Stripe | over-refund | ✅ | n/a |
| Adyen | full | ✅ | ✅ |
| ... | ... | ... | ... |

## Documented gaps

- Braintree disputed-charge refund: deferred to phase 2
- Multi-partial refund crossing day boundaries: manual QA

Anti-patterns

Anti-patternWhy it failsFix
Test only full-refund happy pathPartial-refund accounting bugs hidePer-variant test
Skip over-refund testSum-mismatch on partialsAlways test
No idempotency keyNetwork retry → double refundAlways set
Test refunds against live APIReal moneySandbox-only
Skip async webhook for AdyenRefund status undeterminedWait for [REFUND] notification
Hardcoded refund amountsCents vs dollars confusionPer-currency tests
Test in one currency onlyCross-currency refund quirksTest in EUR, GBP, JPY
Skip cross-tenant refund testsPer cross-tenant-data-leak-tests (in the qa-multi-tenancy plugin), tenant A can't refund tenant B's chargeCross-tenant probe

Limitations

  • Refund-of-disputed-charge is gateway-policy-specific. Some allow with warnings; some block.
  • Currency conversion at refund time may differ from charge time. Test with currency-changing scenarios.
  • Bank-declined refunds simulate poorly; rely on platform- doc'd test cases.
  • No automation for actual money movement. Sandbox refunds don't move real funds; production verification is separate.

References

Related skills

3ds-test-flow-reference

Cross-gateway, protocol-level reference for 3-D Secure (3DS 2.x) test coverage. Covers the EMVCo frictionless / challenge / not-applicable flow paths, SCA under EU PSD2, and the per-PAN test cards for Stripe, Adyen, and Braintree. The gateway-specific wrappers (adyen-test-mode / stripe-test-cards-and-webhooks / braintree-test-cards) compose this skill for single-gateway work, so use one of those for a single-gateway query. Use this skill when designing multi-gateway 3DS test coverage, auditing a 3DS redirect round-trip, or investigating a challenge-flow regression that is not gateway-specific.

adyen-test-mode

Wraps Adyen test-mode patterns: test-API-key initialization, the canonical Adyen test cards (Visa 4111 1111 1111 1111; 5454 5454 5454 5454 3DS frictionless; 4917 6100 0000 0000 3DS 2 challenge), the per-flow result codes (Authorised / Refused / Pending / RedirectShopper / Error), the HMAC-SHA256 webhook validation, and notifications-vs-API duality (Adyen separates synchronous API calls from asynchronous notifications). Use when testing Adyen-integrated code.

braintree-test-cards

Wraps Braintree (PayPal-owned) sandbox testing patterns: sandbox merchant credentials, Drop-in / Hosted Fields client-side patterns, the Transaction lifecycle (submitted_for_settlement → settled), Braintree's distinctive test-card behaviours (specific PANs trigger specific errors), and the webhook verification (Braintree Webhook Parser). Use when testing Braintree-integrated code.

chargeback-flow-test-author

Workflow-driven skill that builds the chargeback / dispute test suite: canonical reason codes (Visa 10.4 fraud, 13.1 services-not-provided; Mastercard MCC 4855), per-gateway dispute APIs (Stripe Disputes, Adyen Chargeback notifications, PayPal Disputes), the evidence-submission flow + window, and disposition outcomes (won / lost / accepted). Use when designing dispute coverage; for refunds use refund-test-matrix-builder, for webhook redelivery + idempotency use payment-webhook-replay, and for the lifecycle state model use payment-flow-states-reference.

payment-flow-states-reference

Pure-reference catalog of payment lifecycle state machines across Stripe, Adyen, PayPal, and Braintree: canonical states (created / requires_action / processing / succeeded / requires_capture / canceled / failed), authorisation vs capture, asynchronous webhook states, and refund / dispute / chargeback transitions. Use when designing tests for payment flows or auditing state-handling code; this is the state model, not a builder - to author suites on it use refund-test-matrix-builder (refunds), chargeback-flow-test-author (disputes), or payment-webhook-replay (webhook replay).

payment-webhook-replay

Workflow-driven skill that builds payment webhook replay + recovery tests. Covers the idempotency contract (every webhook handler must handle redelivery without side effects), the replay simulators (Stripe CLI `stripe trigger`, Adyen Customer Area resend, PayPal Webhook Simulator, Braintree webhook test), the signature-verification gauntlet (HMAC-SHA256 per gateway, expired-timestamps rejection), and the partial-failure recovery scenarios. Use when designing webhook robustness tests.

paypal-sandbox

Wraps PayPal Sandbox testing patterns: sandbox account creation (Business + Personal accounts in developer.paypal.com), the Orders v2 API (create / capture / refund), webhook event simulator (developer.paypal.com webhook simulator), sandbox-account-specific test cards, and the OAuth2 client-credentials flow for sandbox. Use when testing PayPal-integrated code.

pci-dss-scope-reference

Pure-reference catalog of PCI DSS v4.0 scope reduction techniques + the testable scope boundaries. Covers the SAQ levels (A through D, picked by how cardholder data flows), the PAN-storage prohibitions (only first-6 + last-4 retained; nothing else cleartext), the tokenization + hosted-fields scope-reduction patterns (Stripe Elements / Adyen Drop-in / Braintree Hosted Fields keep PAN off your servers), Network-Segmentation as PCI scope-reduction, and the testable behaviours the scope boundary creates. This is the catalog of WHY the boundary matters and what it makes testable - not a checker that verifies a given integration against the standard. Use when designing or auditing the PCI scope of a payment integration.

stripe-subscription-billing-test-author

Builds test suites for Stripe recurring-billing flows: trial-to-paid conversion, proration on plan upgrade and downgrade, dunning on failed renewal, cancel and reactivation, and the full subscription webhook event matrix (invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, invoice.paid). Uses Stripe Billing test clocks (POST /v1/test_helpers/test_clocks) to time-travel through billing cycles without calendar delay. Distinct from stripe-test-cards-and-webhooks (one-time PaymentIntents) and payment-webhook-replay (idempotency + replay robustness). Does not cover single-event CLI replay or handler idempotency testing (see payment-webhook-replay for those). Use when authoring tests for subscription or recurring-billing integrations.

stripe-test-cards-and-webhooks

Wraps Stripe API testing patterns: test-mode initialization, the canonical test cards (4242 success; 4000 0000 0000 0002 declined; 4000 0027 6000 3184 3DS challenge per 3ds-test-flow-reference), the Stripe CLI webhook flow (`stripe listen --forward-to`), the Stripe CLI fixture commands (`stripe trigger payment_intent.succeeded`), and the webhook signature verification (Stripe-Signature header + HMAC-SHA256). Use when testing Stripe-integrated code.