# Tax rate is applied correctly based on customer location jurisdiction

- **Pattern:** `ab-001224` (`ecommerce-shipping-tax.tax-computation.jurisdiction-detection`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001224
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001224)

## Why it matters

A single hardcoded tax rate applied to every location — 8.5% regardless of whether the customer is in California, a no-sales-tax state like Oregon, or a VAT jurisdiction in the EU — is both a CWE-682 calculation error and a regulatory compliance failure. South Dakota v. Wayfair (2018) established economic nexus, meaning multi-state sellers must collect destination-state rates. EU VAT Directive 2006/112/EC imposes separate VAT obligations for digital and physical goods sold in EU member states. Overcharging customers in no-tax jurisdictions exposes the business to refund liability; undercharging in mandatory-tax states exposes it to audit penalties and back-tax assessments.

## Severity rationale

Critical because applying a flat tax rate to all jurisdictions produces regulatory non-compliance with Wayfair nexus rules and EU VAT obligations, exposing the business to both back-tax liability and customer overcharge disputes.

## Remediation

Implement jurisdiction-aware tax lookup in `lib/tax.ts` using a tax service API or a local rate table with at least 5 distinct jurisdiction entries:

```ts
// lib/tax.ts
async function getTaxRate(address: Address): Promise<number> {
  // Option 1: TaxJar (handles Wayfair nexus + VAT automatically)
  const result = await taxjar.rates.get(address.zip, {
    state: address.state,
    country: address.country,
  })
  return result.combined_rate

  // Option 2: Local table (must cover all states you serve)
  const key = `${address.country}-${address.state}`
  return TAX_RATES[key] ?? 0
}
```

Never return a hardcoded constant from `getTaxRate`. If using a local table, ensure it includes at least your nexus states and correctly maps zero-rate jurisdictions (OR, MT, NH, DE, AK) to `0`.

## Detection

- **ID:** `jurisdiction-detection`
- **Severity:** `critical`
- **What to look for:** Before evaluating, extract and quote the tax rate lookup function. Count the number of jurisdiction parameters used: (1) state/province, (2) city, (3) zip code, (4) country. Classify whether rates come from a tax service API, a local lookup table, or a hardcoded constant.
- **Pass criteria:** Tax calculation uses at least 1 jurisdiction parameter (state, province, zip, or country) from the customer's shipping address to determine the applicable tax rate. Rate lookup is performed via a tax service API integration (TaxJar, Avalara, Stripe Tax) or a local rate table with at least 5 distinct rates for different jurisdictions.
- **Fail criteria:** No jurisdiction detection found, or tax rate is a single hardcoded percentage applied to all locations regardless of address. Must not pass when a function accepts an address parameter but ignores it and returns a constant rate.
- **Skip (N/A) when:** The business does not collect sales tax (search for tax-related functions, TaxJar/Avalara dependencies, and tax rate constants; if none found, skip).
- **Detail on fail:** `"Tax rate hardcoded at 8.5% in lib/tax.ts line 12. Function accepts address parameter but ignores it — returns 0.085 for all locations."` or `"Tax lookup table has only 1 entry (US: 8%). No state-level differentiation."`
- **Cross-reference:** Related to `ecommerce-shipping-tax.regional-compliance.regional-tax-rules` (multi-state rules build on jurisdiction detection) and `ecommerce-shipping-tax.checkout-integration.tax-consistency` (displayed tax must use same rate logic).
- **Remediation:** Implement jurisdiction-aware tax calculation in `lib/tax.ts`:

  ```ts
  // lib/tax.ts
  async function getTaxRate(address: Address): Promise<number> {
    // Option 1: TaxJar integration
    const taxJarRate = await taxjar.rates.get(address.zip, {
      state: address.state,
      country: address.country
    })
    return taxJarRate.combined_rate

    // Option 2: Local lookup table
    const key = `${address.country}-${address.state}`
    return TAX_RATES[key] || 0
  }

  function calculateTax(subtotal: number, address: Address, items: OrderItem[]): number {
    const taxableSubtotal = items.reduce((sum, item) => {
      return sum + (item.taxable ? item.price * item.quantity : 0)
    }, 0)

    const rate = getTaxRate(address)
    return Math.round(taxableSubtotal * rate)
  }
  ```

## External references

- cwe CWE-682
- iso-25010 functional-correctness
- external south-dakota-v-wayfair-2018 — https://www.supremecourt.gov/opinions/17pdf/17-494_j4el.pdf
- external eu-vat-directive-2006-112-ec — https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32006L0112

Taxons: regulatory-conformance, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001224
