Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill chargeback-flow-test-author
View source

chargeback-flow-test-author

Overview

A chargeback (also: dispute) is a customer-initiated reversal of a settled transaction. The merchant has a fixed window (typically 7-30 days) to submit evidence. Outcomes: merchant wins (funds retained) or loses (funds + chargeback fee deducted).

Per stripe.com/docs/disputes (opens in new window): "Disputes (also known as chargebacks) start when a cardholder questions a payment with their card issuer." Visa, Mastercard, AmEx each maintain their own reason-code catalogs.

When to use

  • Designing dispute test coverage for a new integration.
  • Auditing existing dispute-evidence-submission code.
  • Building dispute analytics dashboards.

How to use

  1. Pick the 3-5 reason codes most common for your business from references/reason-codes.md.
  2. Trigger a disputable charge in the gateway's test mode (Stripe pm_card_createDispute, an Adyen chargeback notification, a PayPal sandbox dispute).
  3. Assert the dispute lands in needs_response (or the gateway equivalent) with the expected reason.
  4. Submit evidence before evidence_details.due_by and assert has_evidence is set.
  5. Drive each disposition - won (winning evidence), lost (no response), accepted - and confirm the final state via the charge.dispute.closed webhook.
  6. Verify the ledger reverses funds plus the chargeback fee on a lost dispute.
  7. Expand into the (gateway, reason, outcome) matrix so every cell has a test.

Step 1 - Reason code catalog

Visa, Mastercard, and AmEx each maintain their own reason-code catalogs. Pick the 3-5 most-common codes for your business and verify the evidence-collection flow for each - each code has different evidence requirements.

See references/reason-codes.md for the full Visa and Mastercard reason-code tables (Visa 10.4 fraud, 13.1 services-not-provided; Mastercard MCC 4855 non-receipt, and more).

Step 2 - Per-gateway dispute API

Stripe

Per stripe.com/docs/disputes (opens in new window): the dispute object has a status field (needs_response, under_review, won, lost, warning_*).

Test mode:

// Create a disputable charge
const charge = await stripe.paymentIntents.create({
  amount: 1000, currency: 'usd',
  payment_method: 'pm_card_createDispute',  // Special test method
  confirm: true, return_url: 'https://example.com/return',
});

// Wait for the dispute event via webhook
// Or fetch directly
const disputes = await stripe.disputes.list({ payment_intent: charge.id });
expect(disputes.data[0].status).toBe('needs_response');
expect(disputes.data[0].reason).toBe('fraudulent');

Per Stripe testing docs, special payment methods trigger disputes in test mode.

Adyen

Per docs.adyen.com/risk-management/disputes-api (opens in new window): Adyen sends [CHARGEBACK] notifications.

// Simulate via test mode + webhook
const dispute = await disputesApi.acceptDispute({
  disputePspReference: 'test-dispute-ref',
  merchantAccountCode: process.env.ADYEN_MERCHANT_ACCOUNT,
});

PayPal Disputes API

Per developer.paypal.com/docs/api/customer-disputes/v1 (opens in new window):

const dispute = await disputesClient.get(disputeId);
expect(dispute.status).toBe('OPEN');

Step 3 - Submit evidence

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

test('submit dispute evidence', async () => {
  const dispute = disputes.data[0];
  const result = await stripe.disputes.update(dispute.id, {
    evidence: {
      product_description: 'Premium subscription month 5',
      service_documentation: fileUploadId,  // FileUpload object
      shipping_documentation: shippingFileId,
      customer_email_address: 'user@example.com',
      customer_purchase_ip: '203.0.113.1',
      receipt: receiptFileId,
    },
  });
  expect(result.evidence_details.has_evidence).toBe(true);
});

The evidence must be submitted before the due date (evidence_details.due_by).

Step 4 - Test the disposition flow

test('won disputes update internal state', async () => {
  // Trigger via test mode
  const dispute = await createTestDispute({ reason: 'product_not_received' });
  await submitWinningEvidence(dispute.id);

  // In Stripe test mode, certain evidence text wins the dispute
  await waitForWebhook('charge.dispute.closed', { matching: { id: dispute.id } });

  const final = await stripe.disputes.retrieve(dispute.id);
  expect(final.status).toBe('won');
  expect(final.amount).toBe(0);  // No funds deducted
});

test('lost disputes update internal state', async () => {
  const dispute = await createTestDispute({});
  // Don't submit evidence; let it expire
  await skipPastDueDate(dispute);

  await waitForWebhook('charge.dispute.closed');
  const final = await stripe.disputes.retrieve(dispute.id);
  expect(final.status).toBe('lost');
});

Step 5 - Auto-evidence collection

Many merchants auto-collect evidence on every charge - purchase description, shipping info, customer IP - to streamline disputes. Tests should verify:

test('every charge has auto-evidence', async () => {
  const intent = await createSucceededIntent({...});
  const evidence = await loadEvidenceForCharge(intent.charges.data[0].id);
  expect(evidence).toMatchObject({
    customer_email_address: expect.any(String),
    customer_purchase_ip: expect.any(String),
    receipt_url: expect.any(String),
  });
});

Step 6 - Test matrix

# tests/payment/chargeback-matrix.yaml
matrix:
  reason_codes:
    - "Visa 10.4 - fraud"
    - "Visa 13.1 - services not provided"
    - "Mastercard 4855 - non-receipt"
  gateways:
    - stripe
    - adyen
    - paypal
  outcomes:
    - won-with-evidence
    - lost-no-response
    - accepted

Per (gateway, reason, outcome) cell, generate a test.

Step 7 - Reconciliation

Chargebacks affect accounting:

test('lost dispute reverses funds in ledger', async () => {
  const intent = await createSucceededIntent({ amount: 1000 });
  const dispute = await triggerLostDispute(intent);
  await waitForChargebackEvent();

  const ledger = await getLedgerEntries(intent.id);
  expect(ledger).toContainEqual(expect.objectContaining({ type: 'charge', amount: 1000 }));
  expect(ledger).toContainEqual(expect.objectContaining({ type: 'chargeback', amount: -1000 }));
  expect(ledger).toContainEqual(expect.objectContaining({ type: 'chargeback_fee' }));
});

Worked example

A SaaS team on Stripe wants coverage for a Visa 13.1 (services/merchandise not received) dispute they expect to win.

  1. From references/reason-codes.md they map 13.1 to Stripe's product_not_received reason.
  2. A test creates a charge with payment_method: 'pm_card_createDispute', confirms it, lists disputes for the intent, and asserts status === 'needs_response'.
  3. The test uploads a service_documentation FileUpload and calls stripe.disputes.update with product_description: 'Premium subscription month 5' plus the customer email + IP, asserting evidence_details.has_evidence === true and that the call ran before due_by.
  4. It waits for the charge.dispute.closed webhook and asserts status === 'won' and amount === 0 (no funds deducted).
  5. A sibling test skips evidence, advances past the due date, and asserts status === 'lost' plus a chargeback and chargeback_fee pair in the ledger.

Result: the 13.1 won-with-evidence and lost-no-response cells are both green, and the due-date watcher is exercised.

Anti-patterns

Anti-patternWhy it failsFix
Skip dispute tests"Won't happen often" but worst-case impact is highAlways cover
Hand-coded evidence submissionSchema drift; missed fieldsUse gateway SDK helpers
No due-date trackingEvidence submitted late → auto-loseTest the due-date watcher
Test only one reason codeEach reason has different evidence requirementsCover top 3-5
Mock the dispute objectLoses gateway-side state transitionsUse test-mode dispute triggers
Skip chargeback-fee reconciliationBooks don't matchTest the ledger
No webhook handling for charge.dispute.closedFinal state unknownWebhook-driven
Hardcoded "$15 chargeback fee"Per-gateway, per-region; variesRead from gateway response

Limitations

  • Real chargebacks take weeks. Test mode collapses the timeline; production verification needs production traffic.
  • Reason codes change. Visa / Mastercard publish updates; test data goes stale annually.
  • Bank-side evidence review isn't fully testable.
  • Per-jurisdiction rules. EU has stronger consumer protection; US has different.

References

Chargeback reason code catalog

View source (opens in new window)

Chargeback reason code catalog

Pick the 3-5 most-common reason codes for your business and verify the evidence-collection flow for each. Each code carries different evidence requirements, so a single-reason test under-covers the suite. Visa, Mastercard, and AmEx each maintain their own reason-code catalogs.

Visa

Per Visa Chargeback Reason Codes (cite by stable ID: Visa Chargeback Management Guidelines):

CodeCategoryDescription
10.4Fraud"Card-absent environment fraud"
11.1AuthorizationCard recovery bulletin
11.2AuthorizationDeclined authorization
11.3AuthorizationNo authorization
12.1Processing errorsLate presentment
12.2Processing errorsIncorrect transaction code
12.3Processing errorsIncorrect currency
12.4Processing errorsIncorrect account number
13.1Consumer disputesMerchandise/services not received
13.2Consumer disputesCancelled recurring transaction
13.3Consumer disputesNot as described
13.5Consumer disputesMisrepresentation

Mastercard

Per Mastercard MCC chargeback reason codes (cite by stable ID: Mastercard Chargeback Guide):

CodeDescription
4853Cardholder disputes
4855Non-receipt of merchandise
4859Services not rendered
4863Cardholder doesn't recognize

Sources

  • Visa Chargeback Reason Codes (cite by stable ID: Visa Chargeback Management Guidelines).
  • Mastercard Chargeback Guide (cite by stable ID).

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.

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.

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.

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.