Skip to main content

How to Test Returns, Refunds, and Partial Credits

Short answer

Refund bugs double-hit revenue and support. E2E must probe refund rows, Stripe charge.refunded webhooks, and inventory restock—not "Refund submitted" toasts. Seed fulfilled orders per run, control created_at for return-window negatives, and poll probes when webhooks lag after partial line-item refunds.

Part of Testing Guides by business flow.

Who this is for

Ecommerce ops and fintech teams with self-serve returns, partial refunds, store credit, or restocking fees—often backed by Stripe Refunds and async webhooks.

Why testing refunds matters

  • Revenue — double refunds; partial amount wrong; store credit never issued.
  • Compliance — return window policy not enforced; RMA without audit trail.
  • Inventory — restock on refund fails; resell oversell.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Full refundWebhook lagUI pending foreverPoll probe refund status
Partial refundMulti line itemsFull amount refundedProbe sum(refund lines)
Store creditNot Stripe refundLedger wrongProbe credit balance not PI
Return windowOrder 31 days oldStill allowedSeed created_at via Arrange
Restocking feeDeducted from refundWrong netProbe fee line + net refund
Already refundedDouble clickSecond refundProbe idempotent 409
Subscription refundProrated creditWrong periodLink subscriptions guide
Tax on refundPartial tax reversalVAT report wrongProbe tax refunded minor units
Wallet refundPM-specific timingFlaky UIProbe Stripe refund object
ExchangeNew order + creditOrphan exchangeProbe linked order ids
RMA approvalAdmin gateSelf-serve bypassProbe status workflow

Arrange: fulfilled orders

// POST /api/test/seed-order
// { runId, status: 'fulfilled', lines: [{ sku, qty: 2, price: 2500 }], createdAt: '2026-01-01' }

const { orderId } = await request.post('/api/test/seed-order', {
data: { runId, status: 'fulfilled', lineCount: 2 },
}).then(r => r.json());

Complete payment path via Stripe guide when testing full purchase → return journey.

Partial refund E2E

await page.goto(`/orders/${orderId}/return?runId=${runId}`);
await page.getByLabel('Quantity').fill('1');
await page.getByRole('button', { name: 'Submit return' }).click();

await expect.poll(async () => {
const res = await request.get(`/api/test/probes/refunds/${orderId}`);
const body = await res.json();
return body.status;
}, { timeout: 30_000 }).toBe('succeeded');

const refund = await request.get(`/api/test/probes/refunds/${orderId}`).then(r => r.json());
expect(refund.amount).toBe(2500); // one line, not full order
expect(refund.stripeRefundId).toBeTruthy();

Return window negative

await request.post('/api/test/seed-order', {
data: { runId, orderId, createdAt: daysAgo(31) },
});
await page.goto(`/orders/${orderId}/return`);
const probe = await request.post(`/api/test/probes/return-eligibility/${orderId}`).then(r => r.json());
expect(probe.eligible).toBe(false);
// UI should block—optional assert

Stripe test mode refunds

Refunds in test mode behave like prod API—use real stripe.refunds.create in handler under test; no special test card for refund itself once charge succeeded with 4242.

Listen for charge.refunded in webhook tests (webhooks guide).

Store credit path

When refunds issue store credit instead of card reversal:

await page.getByRole('radio', { name: 'Store credit' }).click();
await page.getByRole('button', { name: 'Confirm return' }).click();

await expect.poll(async () => {
const credit = await request.get(`/api/test/probes/store-credit/${runId}`).then(r => r.json());
return credit.balanceCents;
}).toBe(2500);

const refunds = await request.get(`/api/test/probes/refunds/${orderId}`).then(r => r.json());
expect(refunds.stripeRefundId).toBeFalsy();
expect(refunds.type).toBe('store_credit');

CI checklist

  1. Seed fulfilled orders with Stripe charge ids in test mode—avoid replaying full checkout unless testing purchase→return journey
  2. Poll refund probe with 30s timeout for webhook lag
  3. Return-window negatives use Arrange created_at—never wait wall-clock days
  4. Partial and full refund scenarios both run on PRs touching refunds
  5. Assert inventory restock probe when returns restock sellable goods

Anti-patterns

Anti-patternWhy it failsBetter approach
Assert toast onlyRefund never createdProbe refund row
Full refund onlyPartial bugs in prodLine-item partial scenario
Real 30-day waitWindow untestedSeed created_at
No restock assertInventory driftProbe sku qty after refund

Example scenario

Situation: Customer returns one of two items from a fulfilled order.

Expected outcome: Partial refund for one line; other line unchanged; inventory restocked for returned sku.

Why UI-only automation breaks: Return UI shows success but full order amount refunded in Stripe.

  1. Arrange: Seed fulfilled order with two line items for runId.
  2. Act: Self-serve return qty=1 on first line.
  3. Assert: Probe refund amount equals one line; probe inventory + order line return status.

TestChimp workflow: Track refund_type × return_reason in TrueCoverage when store-credit refunds rise in prod.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I test partial refunds in E2E?

Seed fulfilled order with multiple line items, initiate partial return in UI, probe refund amount matches selected lines—not full order total.

Should I wait for charge.refunded webhook?

Yes—poll probe until refund status succeeded with stripeRefundId. UI pending state is not Assert.

How do I test return window expiry?

Seed order created_at beyond policy window via Arrange API, probe return-eligibility false before UI Act.

Store credit vs card refund—different tests?

Yes—store credit probes ledger balance and no Stripe refund id; card refund probes Stripe refund object and payment method credit timing.

How do restocking fees appear in tests?

Probe refund net amount equals line total minus fee; assert fee line item on order audit.

Can I test refunds without replaying full checkout?

Yes—seed fulfilled paid order via test route with Stripe charge id in test mode, then Act return flow only.

Which refund types do prod users actually use?

Compare refund_type × return_reason in TrueCoverage. Run /testchimp evolve when store-credit share grows or policy changes—tag scenarios with // @Scenario:.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo