DEV Community

Daniel Pertu
Daniel Pertu

Posted on

42 tests on a tax routing decision, one mock, and not one call to Stripe

Stripe Managed Payments makes Stripe the merchant of record for a sale, which means it takes on collecting and remitting the VAT. For a small seller that is the difference between registering in several EU member states and not. It is also a per-sale routing decision, and the rule we settled on is narrow:

  • on for customers in the EU-27, where foreign VAT is due from the first sale
  • off for everyone else, including the UK, where domestic registration thresholds apply instead
  • off for an unknown country
  • off entirely unless a master switch is set

None of that is interesting code. It is a set, a lookup and an environment variable. The reason it has 486 lines of tests behind it is that each branch has a consequence denominated in money: route an EU sale as domestic and VAT that was owed was not collected; route a UK sale as EU and the customer is charged tax that was not due. Neither failure shows up as an exception.

So the tests assert the decision, and they assert it without a network.

Test Files  2 passed (2)
      Tests  42 passed (42)
   Duration  209ms
Enter fullscreen mode Exit fullscreen mode

One mock, and it is not Stripe's API

The only thing replaced is our own module that constructs the SDK:

// utils/stripe/api instantiates the Stripe SDK and pulls in the database at
// module scope, so it is replaced wholesale rather than configured.
vi.mock('@/utils/stripe/api', () => ({
  stripe: {
    checkout: { sessions: { create: mocks.create } },
  },
}))
Enter fullscreen mode Exit fullscreen mode

That is the whole test harness. No HTTP interception, no fixture server, no Stripe CLI. The routing half of the code never touches Stripe at all, so those 27 tests import pure functions and call them, and the 15 tests that exercise session creation need exactly one function to be controllable.

Most of the tests assert that nothing happened

The off path does not return null or a flag. It returns empty objects, so the caller can spread them unconditionally:

it('returns empty no-op objects for a UK sale', () => {
  // Spreading these into a session must change nothing at all.
  const mp = managedPaymentsCheckout('GB')
  expect(mp.session).toEqual({})
  expect(mp.productData).toEqual({})
  expect(mp.priceData).toEqual({})
})
Enter fullscreen mode Exit fullscreen mode

That shape is worth the three assertions. The alternative, a conditional at each of the three call sites, is three places to get a tax branch wrong instead of one.

The rest of the negatives:

it('covers exactly the EU-27, and never GB', () => {
  expect(MANAGED_PAYMENTS_COUNTRIES.size).toBe(27)
  expect(MANAGED_PAYMENTS_COUNTRIES.has('GB')).toBe(false)
  expect(MANAGED_PAYMENTS_COUNTRIES.has('US')).toBe(false)
})

it('disables MoR when the country is unknown', () => {
  // Unknown must mean "normal flow", never "guess".
  expect(shouldEnableManagedPayments(null)).toBe(false)
  expect(shouldEnableManagedPayments(undefined)).toBe(false)
  expect(shouldEnableManagedPayments('')).toBe(false)
})

it('treats anything other than the exact string "true" as off', () => {
  // A half-configured env var must fail safe, not half-enable a tax regime.
  for (const value of ['TRUE', '1', 'yes', 'on', '']) {
    vi.stubEnv('MANAGED_PAYMENTS_ENABLED', value)
    expect(isManagedPaymentsEnabled()).toBe(false)
    expect(shouldEnableManagedPayments('DE')).toBe(false)
  }
})
Enter fullscreen mode Exit fullscreen mode

'1' and 'yes' being off is a choice, not an accident. A generous truthiness check is friendly right up to the moment somebody sets the variable to 1 in one environment and true in another and the two environments collect different tax.

And one test that is about bundling rather than tax:

it('reads the switch at call time, not at import time', () => {
  vi.stubEnv('MANAGED_PAYMENTS_ENABLED', 'false')
  expect(shouldEnableManagedPayments('DE')).toBe(false)
  vi.stubEnv('MANAGED_PAYMENTS_ENABLED', 'true')
  expect(shouldEnableManagedPayments('DE')).toBe(true)
})
Enter fullscreen mode Exit fullscreen mode

A module-scope read gets frozen into the build, and then flipping the variable in the hosting dashboard does nothing while appearing to have worked. This assertion fails if somebody hoists the lookup for tidiness.

The retry exists so a tax optimisation cannot take checkout down

If Managed Payments is not actually activated on the account, Stripe rejects every session that carries the parameter. The naive outcome is that every EU checkout returns a 500 and the only sales that complete are the ones outside the EU.

So a rejection of that specific parameter is retried once with the merchant-of-record fields removed, which degrades the sale to an untaxed, flagged one rather than no sale. Eight tests pin that behaviour down, and the interesting ones are about what is not retried:

it('does NOT retry (and rethrows) on a non-MoR error like a card decline', async () => {
  // Retrying here would swallow a failure the buyer needs to see.
  mocks.create.mockRejectedValueOnce({ type: 'StripeCardError', message: 'declined' })
  await expect(
    createCheckoutSession(subscriptionParams(), { managedPayments: true, country: 'DE' })
  ).rejects.toMatchObject({ type: 'StripeCardError' })
  expect(mocks.create).toHaveBeenCalledTimes(1)
})
Enter fullscreen mode Exit fullscreen mode

The matcher that decides this has eight tests of its own, five of which are about what must not match: a card decline, transient connection, API and rate-limit errors, an unrelated invalid-request about a missing price, a non-object, and this one:

it('does NOT match an arbitrary Error that happens to mention the phrase', () => {
  expect(isManagedPaymentsRejection(new Error('managed payments broke'))).toBe(false)
})
Enter fullscreen mode Exit fullscreen mode

Because the first version of that function did a substring search on the message, and a substring search on an error message is a retry condition that anybody can trigger by writing the wrong words in a log line.

Two more assertions cover the strip itself. The retry has to remove the session-level switch and, for a dynamically priced session, a tax behaviour on the price and a tax code on the product, which are nested two levels down. And it has to do that on a clone:

it('does not mutate the original params when stripping MoR', async () => {
  const params = priceDataParams()
  // ... first call rejects, second resolves
  await createCheckoutSession(params, { managedPayments: true, country: 'FR' })

  // Original still carries the MoR fields, so the strip worked on a copy.
  expect(params).toHaveProperty('managed_payments')
  expect(params.line_items[0].price_data.tax_behavior).toBe('exclusive')
})
Enter fullscreen mode Exit fullscreen mode

Along with the assertion that everything else survived, including the price identifier, without which the retry would cheerfully create a session for nothing.

The buyer gets a status code, the logs get the details

it('never leaks invalid-request details to the client', () => {
  const res = checkoutErrorResponse(morRejection())
  expect(res.status).toBe(500)
  expect(res.message).toBe('Failed to create checkout session')
  expect(res.message).not.toContain('managed_payments')
})
Enter fullscreen mode Exit fullscreen mode

Card errors pass through as a 402 with Stripe's own buyer-facing message, because "your card was declined" is actionable. Rate limits map to 429, connection and API errors to a retryable 503, and anything that is our own misconfiguration maps to a generic 500. A buyer who reads "Received unknown parameter: managed_payments" learns something true and useless.

And one function that reports on sales already made

it('flags an EU sale that collected no tax', () => {
  expect(isPossibleMoRMisroute('DE', 0)).toBe(true)
})

it('stays quiet for non-EU sales, where zero tax is correct', () => {
  expect(isPossibleMoRMisroute('GB', 0)).toBe(false)
})

it('does not depend on the master switch', () => {
  vi.stubEnv('MANAGED_PAYMENTS_ENABLED', 'false')
  expect(isPossibleMoRMisroute('DE', 0)).toBe(true)
})
Enter fullscreen mode Exit fullscreen mode

The case it exists for: a visitor whose IP resolves outside the EU is routed as a domestic sale, and then types a German billing address into Stripe's own form. The routing was made on one country and the sale happened in another. Nothing can retroactively fix that session, so this is a review signal rather than a guard, which is why it ignores the master switch. A flag about a sale that already completed must not change its mind because the configuration changed afterwards.

The visible half

The decision above is invisible to a buyer, but its sibling is not. The pricing page shows the two plans in the visitor's own currency, resolved from the country the CDN reports, and the same header that drives that conversion is the one the routing decision reads:

it("reads Vercel's IP geo header", () => {
  const request = new Request('https://pub-trivia.app/api/create-checkout-session', {
    headers: { 'x-vercel-ip-country': 'FR' },
  })
  expect(countryFromRequest(request)).toBe('FR')
})
Enter fullscreen mode Exit fullscreen mode

Turn on a VPN, pick a European exit node, reload the pricing page and the prices change. That is the display half. The routing half, on the same header, is what decides who is merchant of record when you then click through. The structured data on that page quotes a fixed GBP figure regardless, because a crawler has to be told one number and it should not depend on which data centre fetched the page.

If you want to see what the plans are actually buying before any of that, the features list is the page for it, and the free session needs no card and no plan decision at all.

None of the above is tax advice. It is a description of how one small seller encoded a rule it was given, and the only transferable part is the testing shape: when a branch has a consequence that no exception will ever report, write the negative assertions first, and write more of them than feels reasonable.

Top comments (0)