Skip to main content

Add a new tax jurisdiction

When a shop in a new province signs on, the tax engine needs jurisdiction-specific rules. This playbook adds support for one new jurisdiction (e.g., Alberta, Ontario, Quebec).

Drafted from planning · v0.1

Background

Each Canadian province has its own sales-tax structure:

  • BC: GST 5% + PST 7%, with bike + bundle exemptions
  • Alberta: GST 5% only (no PST)
  • Ontario: HST 13% (harmonized; no separate GST/PST)
  • Quebec: GST 5% + QST 9.975%
  • Saskatchewan, Manitoba, NL, NB, NS, PEI: each with their own combinations

Per ADR-0020: BC tax rules in code, rules are coded per jurisdiction, selected by shop_config.tax_jurisdiction.

Steps

Step 1: Research the rules

For the new jurisdiction, document:

  • Rate(s)
  • What's taxable
  • What's exempt (bikes? helmets? services?)
  • Special rules (bundled-with-bike? used-bikes?)
  • Tax registration requirements (do they need a separate ID?)

Reference the provincial government's tax guide. Verify with an accountant.

Step 2: Add the tax helper file

Create src/lib/tax-{abbrev}.js:

// src/lib/tax-ab.js (Alberta example)
export function taxForLine({ tax_category, bundled_with_bike, is_used }) {
// Alberta: GST only, no PST
return { gst: 0.05, pst: 0 };
}

export function jurisdictionMetadata() {
return {
jurisdiction: 'AB',
name: 'Alberta',
gst_label: 'GST',
pst_label: null,
required_registrations: ['GST'],
};
}

Step 3: Add the dispatcher

In src/lib/tax.js (or wherever the dispatcher lives):

import * as bc from './tax-bc.js';
import * as ab from './tax-ab.js';
import * as on from './tax-on.js';

const ENGINES = { BC: bc, AB: ab, ON: on };

export function taxForLine(line, shop_config) {
const engine = ENGINES[shop_config.tax_jurisdiction];
if (!engine) throw new Error(`No tax engine for ${shop_config.tax_jurisdiction}`);
return engine.taxForLine(line);
}

Step 4: Update the shop config form

The Settings → Shop form should let the operator select the jurisdiction. The dropdown is sourced from the registered engines.

Step 5: Update receipts and reports

Receipt PDFs use the jurisdiction's labels (e.g., "HST" instead of "GST + PST"). The receipt template reads these from shop_config + the engine's metadata.

Sales-tax CSV exports break out by the jurisdiction's tax categories.

Step 6: Unit tests

Add test/unit/tax-{abbrev}.test.js with known scenarios:

  • New bike → expected tax
  • Service labour → expected tax
  • Component bundled with bike → expected tax
  • Edge cases per jurisdiction

Step 7: Bible

Update:

  • docs/decisions/0020-bc-tax-rules-in-code.md — note the additional engine
  • docs/domain/entities/sku-and-variant.md — note the jurisdiction-driven tax_category effects
  • Optionally: a new ADR if the new jurisdiction has surprising rules

Step 8: Test with the shop

Before going live with the new shop:

  • Walk the owner through how tax displays on a sample sale
  • Verify the tax IDs are entered correctly
  • Run a few test transactions, reconcile the totals manually with the owner

Common gotchas

  • HST vs GST+PST: Some provinces use HST (single tax); others use GST + provincial. The receipt label and CSV columns must match.
  • Used-bike rules: Vary widely. BC exempts; others may not.
  • Bundle rules: Unique to BC. Other provinces tax bundles fully.
  • Border sales: A shop near a provincial border may sell to customers in another province (mail-order). Helm doesn't currently handle the inter-provincial tax rules; those are sold at the shop's province's rate. Flag for legal review when needed.

See also