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
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Full refund | Webhook lag | UI pending forever | Poll probe refund status |
| Partial refund | Multi line items | Full amount refunded | Probe sum(refund lines) |
| Store credit | Not Stripe refund | Ledger wrong | Probe credit balance not PI |
| Return window | Order 31 days old | Still allowed | Seed created_at via Arrange |
| Restocking fee | Deducted from refund | Wrong net | Probe fee line + net refund |
| Already refunded | Double click | Second refund | Probe idempotent 409 |
| Subscription refund | Prorated credit | Wrong period | Link subscriptions guide |
| Tax on refund | Partial tax reversal | VAT report wrong | Probe tax refunded minor units |
| Wallet refund | PM-specific timing | Flaky UI | Probe Stripe refund object |
| Exchange | New order + credit | Orphan exchange | Probe linked order ids |
| RMA approval | Admin gate | Self-serve bypass | Probe 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
- Seed fulfilled orders with Stripe charge ids in test mode—avoid replaying full checkout unless testing purchase→return journey
- Poll refund probe with 30s timeout for webhook lag
- Return-window negatives use Arrange
created_at—never wait wall-clock days - Partial and full refund scenarios both run on PRs touching refunds
- Assert inventory restock probe when returns restock sellable goods
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Assert toast only | Refund never created | Probe refund row |
| Full refund only | Partial bugs in prod | Line-item partial scenario |
| Real 30-day wait | Window untested | Seed created_at |
| No restock assert | Inventory drift | Probe 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.
- Arrange: Seed fulfilled order with two line items for runId.
- Act: Self-serve return qty=1 on first line.
- 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).
Related scenarios
- Stripe payments — original capture
- Inventory — restock
- Stripe webhooks — charge.refunded
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.