Back to BlogQuality & Testing

Testing Tax and Shipping: The Edge Cases That Overcharge

Rupak Amin

Founder & Lead Engineer, RAITHub

10 min read

Tax and shipping bugs overcharge quietly: a rounding rule that adds a cent per line, a discount applied after tax instead of before, a free-shipping threshold that counts the post-discount total, or a mixed-rate basket taxed at one blended rate. Test them with table-driven cases that lock the exact total for known baskets, then re-run the same cases after every change, so a refactor cannot move the money.

If you would rather have the suite built and run for you, see how RAITHub would test this below. First, the cases that catch the leaks.

Why do tax and shipping bugs cost real money?

Because they are small and constant. One cent of over-tax per line, across thousands of orders, is a steady loss of trust and a stream of refund requests; one cent under is a liability you discover at audit. Shipping is worse, because a wrong threshold or zone can give away delivery or overcharge for it on every affected order. None of this shows up in a smoke test that just reaches the thank-you page. It shows up only when you assert the exact total for a basket you control.

Edge caseThe bugWhat the test asserts
RoundingRounding each line then summing gives a different total than summing then roundingThe order total matches a hand-computed figure to the cent, under your stated rounding rule
Mixed tax ratesA basket of standard and reduced-rate items is taxed at one blended rateEach line is taxed at its own rate; the tax total equals the sum of line taxes
Discount vs tax orderAn order discount is applied after tax, so the shopper is taxed on money they did not payThe discount reduces the taxable base before tax, per your rule
Free-shipping thresholdThe threshold is checked against the post-discount total, so a coupon removes free shippingThe threshold is measured against the agreed basis, the same way every time
Partial refundRefund reverses a proportion of tax, not the tax on the returned linesThe tax refunded equals the tax originally charged on the returned units

How should rounding be tested?

Pick one rule and assert it. The common choice is to compute tax per line, round each line, then sum, but "round the order total once" is also valid, and the two disagree by a cent on some baskets. Whichever you choose, write cases whose correct answer you worked out by hand, so the test fails if the code quietly switches rules.

import { describe, it, expect } from 'vitest'
import { priceOrder } from './pricing'

describe('rounding', () => {
  // Three lines at a price whose tax does not divide evenly.
  it('rounds per line then sums, to the stated rule', () => {
    const r = priceOrder({
      lines: [
        { qty: 1, unitExTax: 3.33, taxRate: 0.2 },
        { qty: 1, unitExTax: 3.33, taxRate: 0.2 },
        { qty: 1, unitExTax: 3.33, taxRate: 0.2 },
      ],
    })
    // 3.33 * 0.2 = 0.666 -> 0.67 per line, * 3 = 2.01 tax
    expect(r.taxTotal).toBe(2.01)
    expect(r.grandTotal).toBe(11.00)   // 9.99 + 2.01, rounded to the rule
  })
})

How do you test discounts, tax order and free shipping together?

With a small grid of baskets that combine the rules, because the bugs live where they meet. The one that bites most often is a coupon that drops the basket just under the free-shipping threshold: if the threshold is checked after the discount, the shopper loses free shipping they expected, or gains it when they should not.

describe('discount, tax order and free shipping', () => {
  it('applies the discount before tax', () => {
    const r = priceOrder({
      lines: [{ qty: 1, unitExTax: 100, taxRate: 0.2 }],
      orderDiscount: 10,            // fixed amount off, before tax
    })
    expect(r.taxableBase).toBe(90)
    expect(r.taxTotal).toBe(18)     // tax on 90, not 100
  })

  it('measures free shipping against the pre-discount subtotal', () => {
    const r = priceOrder({
      lines: [{ qty: 1, unitExTax: 55, taxRate: 0 }],
      orderDiscount: 10,            // drops the total below 50
      freeShippingThreshold: 50,
      freeShippingBasis: 'preDiscountSubtotal',
    })
    expect(r.shipping).toBe(0)      // the subtotal was 55, so shipping stays free
  })
})

The order of operations here, whether a percentage or a fixed discount applies first, and whether it applies before or after tax, must match the rules in your promotions engine. If you have a separate coupon and promotions engine, the pricing tests and the promotion tests should agree on that order, or the total will differ depending on which path computed it.

What shipping cases are easy to miss?

  • Zones and the fallback. An address that matches no zone should hit a defined fallback, not crash or ship free. Test an address outside every configured zone.
  • Weight and dimension bands. An item exactly on a band boundary should fall on the side your rule says. Test the boundary, not just the middle of each band.
  • Mixed baskets. A basket with one heavy and one light item should price shipping on the combined rule, not the first item.
  • Free-shipping items. A basket mixing a free-shipping product and a normal one should charge for the normal one only, per your rule.
  • Tax on shipping. In some jurisdictions shipping is taxable. Decide the rule and test it; do not leave it to chance.

Shipping and tax often come from a third-party provider. Test your integration with the provider in a sandbox, and keep your own cases for the parts you compute, so a provider change is caught the same release. End-to-end, the checkout should show the same total the server charges, which is where a checkout bug quietly costs sales.

How should these tests run?

As fast unit tests on a pure pricing function, plus a thin end-to-end layer. The pricing function takes a basket and settings and returns every number, so it needs no browser and runs in milliseconds, which means you can afford hundreds of cases. Then a few Playwright tests confirm the displayed total equals the charged total and survives applying a coupon. Gate both in CI, so a change that moves a total blocks the release instead of reaching shoppers. This is the testing discipline in the pre-launch QA checklist.

Buy, build or hire?

OptionChoose this whenThe catch
Trust the platform or tax providerA hosted store with a standard tax provider and simple shippingYou still own the seam: discounts, thresholds and rounding where your rules meet theirs
Write your own test suiteYou have a custom cart and the maths is yoursSomeone has to enumerate the cases and keep them current
Hire QA to build and run itTotals have drifted, or a launch or big release is coming and the maths must be provably rightA one-off audit is a checkpoint; ongoing drift needs tests in CI

How long does it take to do yourself, and what is the risk?

For a developer who knows the codebase, writing a solid pricing test suite is 3 to 5 days: enumerate the rules, write table-driven cases, and add a few end-to-end checks. The main risk is unstated rules. If nobody has written down the rounding rule, the discount-before-or-after-tax rule and the free-shipping basis, the tests encode a guess. Agree those rules in plain language first; the tests then make them permanent. Tax rules vary by jurisdiction, so confirm the rates and treatment with your tax adviser; this is general information.

Why RAITHub for this

  • Money-path testing in production. Sundor Skin, a B2B wholesale platform RAITHub built, computes tier pricing, tax and credit on the server and tests it across 146 PostgreSQL tables and 530+ tests. See the Sundor Skin case study.
  • Totals are gated. PropDesk, a property-management SaaS RAITHub built, carries 1,024 tests, including the money paths behind Stripe rent collection, gated in CI. See the PropDesk case study.
  • The testers also build. Bugs come with a failing test and a suggested fix, because the same engineers write and fix the code.

When you don't need us

  • Platform plus provider fits. A standard hosted store with a mainstream tax provider may need only a handful of your own cases.
  • You already have the suite. The cases above are a fair start for your own developer.
  • Volume is tiny. A careful manual check each release may be enough until it is not.

How RAITHub would test this

  • Pin the rules: write down rounding, discount-vs-tax order, free-shipping basis and tax-on-shipping in plain language, approved by you.
  • Table-driven unit tests: hundreds of baskets with hand-checked totals on a pure pricing function.
  • Integration tests: the tax and shipping provider in a sandbox, so a provider change is caught the same release.
  • End-to-end checks: the displayed total equals the charged total, and survives coupons and refunds, gated in CI.

Timeline: as a fixed-scope one-off audit this is a short engagement; as ongoing protection it runs as a monthly QA plan. See QA as a Service, QA and test automation and the ecommerce industry page.

You receive: the test suite in your repository, CI gates that block a release when a total moves, a report of what was found, and full IP in your name under NDA.

Next step: book the free 15-minute technical audit with a few real baskets and your pricing rules, and we will follow up with a written fixed quote.

Frequently asked questions

Why is my checkout overcharging tax by a cent?

Almost always rounding. Rounding each line then summing gives a different total than summing then rounding. Pick one rule, and assert the exact total for hand-checked baskets so a refactor cannot switch rules unnoticed.

Should a discount apply before or after tax?

Usually before, so the shopper is taxed on what they actually pay, but it depends on jurisdiction. Decide the rule, write it down, and test it. Confirm the correct treatment with your tax adviser; this is general information.

Why does a coupon remove my free shipping?

Because the free-shipping threshold is checked against the post-discount total. Decide whether the threshold uses the pre- or post-discount subtotal, and test a basket that a coupon pushes across the threshold.

How many tax and shipping test cases do I need?

Enough to cover each rule and the boundaries where rules meet: rounding, mixed rates, discount order, threshold edges, zone fallbacks and band boundaries. A pure pricing function runs in milliseconds, so you can afford hundreds.

Can I rely on my tax provider to be correct?

Trust it for the rates, but test the seam you own: discounts, thresholds, rounding and how your cart feeds the provider. Run the provider in a sandbox in your integration tests so a provider change is caught before release.

Is this a one-off audit or ongoing testing?

Both are offered. A one-off audit is a checkpoint before a launch or big release. If totals keep drifting, the tests belong in CI as a monthly QA plan, so a change that moves a total blocks the release.

tax calculation testingshipping calculation testingecommerce qarounding bugscheckout testingtest plan

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.