main. It pairs backend orchestration, the payment
gateway, the checkout UI, and the storefront — a missed regression in any of these costs
real orders.
Cadence
- Per PR touching cart, order, payments, or any PSP module: run the smoke sequence
- Weekly against production: full matrix (smoke + cross-PSP + idempotency)
- Pre-release for every backend tag: full matrix + chaos drills
1. Test fixtures
A reliable stability program needs known-good and known-broken fixtures so every regression test has predictable inputs.Fixture shops
| Slug | Purpose | Shipping profile | Payment methods |
|---|---|---|---|
qa-physical-easypost | Real EasyPost rates, physical goods, US→US | EasyPost | Stripe (test), PayMob (sandbox), Bonum (sandbox) |
qa-physical-printful | POD goods via Printful | Printful | Stripe (test) |
qa-physical-uae | UAE→MENA, exercises Telr + PayMob | Custom flat-rate by country | Telr (sandbox), PayMob (sandbox), Stripe (test) |
qa-digital-only | Pure digital items, no shipping | none | Stripe (test), Bonum (sandbox) |
qa-mixed | Physical + digital, split shipping | EasyPost (physical only) | Stripe (test) |
qa-crypto | Web3 product, on-chain settlement | none / digital | x402 / Coinbase Commerce / SOL Pay |
Fixture customer accounts
Test-mode-only — never real cards.| Card | Address country | Why | |
|---|---|---|---|
qa+stripe-pass@... | 4242 4242 4242 4242 | US | Happy path |
qa+stripe-3ds@... | 4000 0025 0000 3155 | US | Forces 3DS challenge |
qa+stripe-decline@... | 4000 0000 0000 0002 | US | Generic decline |
qa+stripe-insufficient@... | 4000 0000 0000 9995 | US | Insufficient funds |
qa+stripe-disputed@... | 4000 0000 0000 0259 | US | Triggers dispute post-charge |
qa+stripe-mena@... | 4242 4242 4242 4242 | AE / SA / EG | AVS variations, currency conversion |
Fixture environments
- Stage (
apiv3stage+checkoutstage) — primary smoke target. Hits Stripe test mode, EasyPost test API, PayMob sandbox, Telr sandbox. - Production (
apiv3prod+checkout.droplinked.io) — full live target. Run from a UAE-IP runner for IP-allowlist coverage. - Local docker compose — backend-only assertions; can’t exercise full Stripe Elements.
Fixture freshness
Schedule a weekly clean ofqa-* shop carts older than 7 days, void all qa+* test
charges, and archive completed test orders. Stops fixture rot from masking regressions.
2. Per-PR smoke sequence
Twelve assertions. Should complete in under 10 minutes against stage.| # | Scenario | Fixture | Expected |
|---|---|---|---|
| S1 | Add 1 physical item → checkout init | qa-physical-easypost | 200, cart ID returned, totals match items + 0 shipping |
| S2 | Enter US shipping address → availableShipping populated | + US addr | ≥1 shipping group, ≥1 rate per group with rateId + price + carrier |
| S3 | Select shipping rate → totals update | continued | totals.shipping reflects rate; totals.total = items + shipping + tax |
| S4 | Create Stripe PaymentIntent → client_secret | continued | clientSecret + paymentIntentId present, no shipping mismatch |
| S5 | Pay with 4242… → order moves to PAID | continued | Order flips PENDING→PAID within 30s of webhook |
| S6 | Stripe charge has populated branding fields | Stripe dashboard test mode | statement_descriptor_suffix, receipt_email, shipping block populated |
| S7 | Digital-only cart skips shipping entirely | qa-digital-only | Frontend hides shipping section; checkout completes with billing only |
| S8 | Mixed cart splits shipping correctly | qa-mixed | Only physical item in availableShipping; digital is unshippable but non-blocking |
| S9 | EasyPost-tagged product gets real carrier rates | qa-physical-easypost, US→US | USPS / UPS / FedEx appear; no silent fallthrough to custom strategy |
| S10 | Printful-tagged product gets Printful rates | qa-physical-printful | Printful strategy fires; no EasyPost call |
| S11 | 3DS challenge card completes payment | qa+stripe-3ds@… | Redirects to 3DS challenge, returns, charge completes |
| S12 | Decline card lands clean error | qa+stripe-decline@… | Generic “card declined” surfaced, cart preserved, no order created |
3. Cross-PSP matrix (weekly)
Run the same 12 scenarios across each enabled PSP for the relevant currency.| PSP | Currency | Origin | Dest |
|---|---|---|---|
| Stripe | USD | US | US |
| Stripe | AED | AE | AE |
| Stripe | AED | AE | SA |
| PayMob | EGP | EG | EG |
| PayMob | AED | AE | AE |
| Telr | AED | AE | AE |
| Bonum | MNT | MN | MN |
| Coinbase Commerce | USDC | global | digital (digital-only carts) |
| x402 / SOL Pay | USDC | global | digital (digital-only carts) |
| PayPal | USD | US | US |
- Time-to-complete checkout (p50, p95)
- Webhook latency (PSP → integration service → backend)
- Order status transition path
- Cart-preserved-on-failure behavior
4. Idempotency + replay matrix
Every PR that touches webhook handlers or cart-status transitions must pass:| # | Scenario | Expected |
|---|---|---|
| I1 | Same webhook delivered twice (Stripe replay) | Second is no-op; no double-paid; no duplicate emails |
| I2 | Webhook arrives before payment-intent succeeded callback | Backend resolves to correct final state regardless of arrival order |
| I3 | Webhook arrives 24h late (Stripe retries) | Still processed correctly; idempotency key honored |
| I4 | Two concurrent “Pay Now” clicks | One PaymentIntent created; second click 4xxs cleanly (no double-charge) |
| I5 | User abandons checkout, returns next day | Cart preserved if not expired; expired carts show empty-state, not 500 |
| I6 | Selected shipping rate expires (EasyPost ~15min TTL) | Backend revalidates; customer asked to reselect; no silent total drift |
| I7 | Network failure mid-payment (Stripe requires_action → connection drop) | Order reconciles via webhook within 5 minutes |
5. Chaos drills (pre-release / monthly)
Test failure modes that only happen in production.| # | Drill | How to simulate | Pass criteria |
|---|---|---|---|
| C1 | EasyPost insufficient funds | Drain EasyPost wallet in sandbox | Backend alerts ops; existing checkouts fall back to flat-rate; new checkouts surface a graceful banner |
| C2 | Stripe 5xx | Toxiproxy outage simulation | Backend retries once, then fails gracefully; cart preserved |
| C3 | Mongo Atlas connection drop | Block Atlas IP in test env | Backend serves cached cart; surfaces error on writes; no 500s to reads |
| C4 | Integration service down | Stop the 3rdp container | Backend opens circuit breaker after 3 failures; surfaces “Payment provider unavailable” within 2s |
| C5 | Webhook signature failure (rotated key) | Send webhook with stale HMAC | 401, log, alert; do NOT process the event |
| C6 | Restricted Stripe key from unauthorized IP | Spin up runner without IP allowlist | Stripe returns 403; backend should not retry-loop |
| C7 | UAE→US Stripe currency conversion | AED-priced order, US card | Conversion happens once; receipt + dashboard agree on FX rate |
| C8 | Mixed cart with one out-of-stock item | Stock one item to 0 mid-checkout | Item removed with clear message; cart total recomputes; selected shipping rate revalidates |
6. Known regression — EasyPost silently uses custom strategy
Fix path:Implement an EasyPost shipping strategy
Wrap the
easy-post module’s rate-fetch in a ShippingRateStrategy adapter.Register it ahead of the custom strategy
Add it to the strategy factory’s default-initialization list before the custom strategy.
Tighten canHandle
Update the custom strategy to only return
true for explicit 'custom' / 'flat-rate'
identifiers — never as a catch-all.7. Tooling roadmap
What we have:- A narrowly-Bonum stress-test script — pattern-extend for other PSPs
- Some payment-gateway specs needing repo-level jest cleanup
- A backend CI workflow with lint+test as a soft-fail
Playwright suite for S1–S12
Drive Stripe Elements test-mode card entry; assert on backend cart state via direct
API checks between UI steps.
GitHub Actions workflow
checkout-smoke.yml runs the Playwright suite on every PR labelled area:checkout or
touching cart/order/payments modules.Tagged Stripe test-mode dashboard
Isolate regressions from real-money flow with a dedicated dashboard tag.
Weekly UAE-egress cron
Run sections 2 + 3 + 4 against production weekly from a UAE-IP runner; post to a
#stability channel.