How to Test Tax, VAT, and Country-Specific Pricing
Short answer
Tax errors create compliance exposure and cart abandonment. E2E must probe order line items and tax amounts in minor units for seeded billing countries and VAT IDs—not translated "Tax" labels. Combine geo fixtures, Stripe Tax test mode where used, and TrueCoverage on country × tax_jurisdiction × currency slices prod actually checks out with.
Part of Testing Guides by business flow.
Who this is for
Global ecommerce and SaaS with localized prices, VAT/GST, US sales tax, or B2B reverse-charge who bill through Stripe Tax, Avalara, or custom rate tables.
Why testing tax matters
- Compliance — wrong VAT on B2B exempt accounts; missing OSS reporting data.
- Revenue — under-tax erodes margin; over-tax abandons carts.
- Support — "Price changed at checkout" from geo IP vs billing address mismatch.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| EU B2C VAT | DE billing address | Rate wrong | Seed DE postal; probe tax rate |
| B2B reverse charge | Valid VAT ID | Tax still applied | Seed exempt org + VIES-valid ID |
| US nexus | State + zip | Surprise tax | Seed CA zip; probe state tax line |
| Geo IP vs billing | VPN country | Wrong price book | Fix billing country in Arrange |
| Currency JPY | No decimals | Float drift | Probe integer minor units |
| Rounding | Line vs order total | 1¢ off | Probe sum(lines) === total |
| Inclusive vs exclusive | Display vs charge | Customer dispute | Probe tax_behavior on lines |
| Digital services MOSS | Cross-border EU | Wrong jurisdiction | Seed EU customer countries |
| Price book | Country-specific SKU | Default USD shown | Seed geo; probe price_id |
| Tax-exempt cert | Expired cert | Tax removed incorrectly | Seed cert expiry date |
| Subscription tax | Recurring invoices | First invoice only tested | Probe renewal invoice tax |
| Refund tax | Partial refund | Tax not reversed | See returns guide |
Arrange: geo and tax fixtures
// POST /api/test/seed-customer-tax-profile
// { runId, country: 'DE', postalCode: '10115', vatId: 'DE123456789', taxExempt: false }
await request.post('/api/test/seed-customer-tax-profile', {
data: { runId, country: 'DE', postalCode: '10115' },
});
Override geo IP in test env (X-Test-Country: DE) so tests do not depend on CI runner location.
EU VAT checkout
await request.post('/api/test/seed-customer-tax-profile', {
data: { runId, country: 'DE', postalCode: '10115' },
});
await page.goto(`/checkout?runId=${runId}`);
// Complete address + payment — card path from stripe guide
await completeStripeCheckout(page, runId);
const order = await request.get(`/api/test/probes/orders/${runId}`).then(r => r.json());
expect(order.taxLines[0].jurisdiction).toMatch(/DE/i);
expect(order.taxLines[0].rate).toBeCloseTo(0.19, 2);
expect(order.totalTax).toBe(Math.round(order.subtotal * 0.19));
B2B exempt
await request.post('/api/test/seed-customer-tax-profile', {
data: { runId, country: 'DE', vatId: 'DE811128135', taxExempt: true },
});
// ... checkout ...
const order = await request.get(`/api/test/probes/orders/${runId}`).then(r => r.json());
expect(order.totalTax).toBe(0);
expect(order.reverseCharge).toBe(true);
Currency rounding
| Currency | Assert |
|---|---|
| USD | Cents integer |
| JPY | Zero decimal places |
| BHD | 3 decimal minor units |
Probe amount_minor fields—never toBeCloseTo on floats without minor-unit conversion.
Stripe Tax
If using Stripe Tax, enable test mode tax registrations in dashboard; probe Checkout Session total_details.amount_tax via your backend mirror.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Assert UI "VAT 19%" | i18n/copy changes | Probe tax line rate |
| Single US address only | EU prod majority | Seed country matrix |
| Hard-code rates | Law changes | Probe against config version id |
| Skip exempt path | B2B invoices wrong | Dedicated exempt scenario |
Example scenario
Situation: German B2B customer with valid VAT ID checks out.
Expected outcome: Reverse charge applied; invoice shows zero VAT and valid VAT IDs.
Why UI-only automation breaks: Checkout shows €0 tax but invoice PDF still includes VAT line.
- Arrange: Seed org with DE VAT ID and taxExempt verified.
- Act: Complete checkout with billing address in DE.
- Assert: Probe totalTax=0, reverseCharge=true, customer and seller VAT on invoice metadata.
TestChimp workflow: Compare country × tax_jurisdiction × currency in TrueCoverage—expand when DE B2B share grows.
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 — Checkout tax lines
- Localization — display vs charge
- Subscriptions — recurring tax
External references
Frequently asked questions
How do I test VAT for EU countries in E2E?
Seed customer with EU billing country and VAT ID, complete checkout, probe order tax lines match expected rate for that jurisdiction—do not hard-code UI labels only.
How do I test B2B reverse charge?
Seed validated VAT ID with taxExempt flag, checkout, probe totalTax zero and reverseCharge or equivalent metadata on order and invoice.
Geo IP shows wrong country in CI—what do I do?
Set test header or seed billing address in Arrange; never rely on CI runner IP for tax assertions.
Should tax tests use Stripe Tax or mock rates?
Integration E2E should use same engine as prod—Stripe Tax test registrations or your Avalara sandbox—not hard-coded mocks in E2E.
How do I test JPY rounding?
Seed JPY price book, checkout, probe all amounts integer in minor units with zero decimal currency exponent.
Do subscriptions need separate tax scenarios?
Yes—first invoice and renewal can differ when address changes; probe invoice.tax on renewal after test clock advance.
We sell in 12 countries—how do we prioritize test coverage?
Use TrueCoverage on country × tax_jurisdiction × currency from prod checkout metadata. Run /testchimp evolve for missing top slices; link // @Scenario: per country.
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.