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).
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 enginedocs/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.