Skip to content

Adding a Province

Provinces (and, by the same pattern, states / regions) are static catalog data in src/registry/provinces/. The app resolves user.provinceId through ProvinceRegistry and exposes the active row as storeprovinceDef for tax hints, expense categories, and platform allow-lists.

Country-first overrides: when a province row exists, its availablePlatforms (and future per-province keys) win over the country’s defaultAvailablePlatforms — see Market Resolution. If no province row matches (countryId, provinceId), store.provinceDef may be null; features should tolerate that. Folder layout: src/registry/provinces/{CA|US|…}/**/*.province.js — one country folder per ISO market, then one module per subdivision (see CA/ON.province.js and US/TX.province.js).

For registry philosophy, see the Registry Architecture.


  1. Create folder src/registry/provinces/{ISO}/ if it does not exist (two-letter country id, same as CountryRegistry).
  2. Copy src/registry/provinces/CA/_TEMPLATE.province.js to src/registry/provinces/{ISO}/{CODE}.province.js (e.g. CA/BC.province.js, UK/ENG.province.js). For US states, add US/{CODE}.province.js using the one-line pattern in an existing state file (see US/_usStateProvince.js + e.g. US/TX.province.js). Use a short uppercase id inside the file for the subdivision code.
  3. Set countryId to a country that already exists in CountryRegistry (CA, US, …).
  4. Fill labelKey (e.g. provinces.bc) and add the same path under strings.en and strings.fr in src/utils/strings.jst() walks dot segments, so provinces.bc maps to nested provinces → bc string leaves (mirror every key in both locales).
  5. Set availablePlatforms to an array of PlatformRegistry ids (same strings as doordash.platform.js id). Only listed platforms are treated as “available” for that market when building picker / province-driven UX.
  6. Define expenseCategories (recommended): each row needs stable id, i18n labelKey (usually under expenses.cat.* — see CA/ON.province.js), and optional craLine, mixedUse, vehicleTypes for driver guidance. These feed getAllCategories() when store.get('provinceDef') is set.
  7. Optional blocks: salesTax, incomeTax, pensionContribution, vehicleExpenseMethod, referenceUrl, vehicleNotes, onboardingExtras — mirror the shape used in CA/ON.province.js where applicable.
  8. Run npm run rebuild:provinces so index.js imports the new file (all country folders are scanned). For US states using the shared factory, also add a row to withholding-presets.js when a withholding hint applies.
  9. Run node build.js --prod and ensure startup validation passes (assertProvinceRegistryValid in main.js).

Regenerate index.js (all countries) + US stubs

Section titled “Regenerate index.js (all countries) + US stubs”

Province modules live under src/registry/provinces/{ISO}/ (e.g. CA/, US/, UK/). After you add or remove a *.province.js there, or you change WITHHOLDING_PRESETS_US, run:

Terminal window
npm run rebuild:provinces

This rewrites index.js: it scans every two-letter country folder and imports each *.province.js except names starting with _ (templates). Canada: Ontario stays first in the CA/ list, then other provinces A–Z. US: order follows the withholding map; missing US/{CODE}.province.js stubs are created (factory one-liner). Other countries (e.g. UK/) are picked up automatically once the folder and files exist.

To overwrite every US stub:

Terminal window
node scripts/rebuild-province-index.js --force-us

Then run npm run build as usual.

validateProvinceDefinition requires:

FieldRule
idPresent; looked up with .toUpperCase() — keep ids uppercase in the file.
countryIdPresent; must match how you filter with ProvinceRegistry.getByCountry('CA').
availablePlatformsNon-empty array of platform id strings.
expenseCategoriesRequired key; use a non-empty list for real provinces (each entry: id, labelKey, optional craLine, mixedUse, vehicleTypes). An empty array [] is valid for the validator but useless for drivers.

Unknown ids: ProvinceRegistry.getById(x) falls back to FALLBACK_ID (ON) when the id is missing from the map. If your primary market is no longer Ontario, consider changing that fallback in index.js deliberately (it affects every unresolved provinceId).


String passed to t(). Add nested keys in strings.js for both locales so UI does not show raw key paths.

Subset of PlatformRegistry ids. If you add a new platform and want it in this province, add the platform file first, then include its id here.

Used when merging province-first categories in getAllCategories(): province rows come first; remaining global registry categories fill gaps. id values should align with expense category values saved in Dexie where possible.

Optional objects for HST/GST/PST-style metadata and rough marginal brackets (planning / UI — not legal advice). Copy structure from ON and adjust keys, rates, and labelKey / infoKey string ids.

Small declarative hooks for onboarding (see ON: HST registration toggle). The orchestrator must know how to interpret each type; adding a new type requires code changes in onboarding, not only data.

Optional strings for province-specific copy keys and CRA/help links.


AreaWhat to do
User defaultDEFAULT_USER / migrations — if a new province should be the default for new vaults, set provinceId (and countryId / locale) consistently.
StoresyncLocaleDefsFromUser already loads provinceDef from getProvinceDef(user.provinceId) — no change needed if only catalog data was added.
OnboardingCurrent v3 onboarding is Ontario-oriented in steps.js (fixed taxRegion / CA flow). Adding provinces for multi-region onboarding means new steps or selectors — not automatic from the def file alone.
Tax moduleProvince-aware summaries read country + user + expense data; heavy province logic may still need updates in tax.js for new regimes.

src/registry/provinces/BC.province.js
export default {
id: 'BC',
countryId: 'CA',
labelKey: 'provinces.bc',
availablePlatforms: ['doordash', 'ubereats', 'skip', 'instacart', 'other'],
salesTax: { name: 'GST+PST', rate: 0.12, /* … */ },
incomeTax: { suggestedSetAsidePct: 25, brackets: [/* … */] },
expenseCategories: [
{ id: 'fuel', labelKey: 'expenses.cat.fuel', craLine: '' },
// …
],
vehicleExpenseMethod: 'actual_costs',
onboardingExtras: [],
};

Then in index.js:

import BC from './BC.province.js';
const PROVINCES = [ON, BC];

  • Set user.provinceId to the new code (Settings or Dexie) and reload: store.provinceDef.id should match.
  • Open Expenses → add: category grid should prefer the province expenseCategories list.
  • Grep for hardcoded 'ON' outside migrations/fallbacks; replace with provinceDef or user.provinceId where appropriate when you truly support multiple provinces.

FileRole
src/registry/provinces/CA/_TEMPLATE.province.jsEmpty-ish starter def (Canada).
src/registry/provinces/CA/ON.province.jsFull reference implementation (Canada).
src/registry/provinces/US/_usStateProvince.jsShared factory for default US state rows.
src/registry/provinces/index.jsRegistry + validation + fallback.
src/utils/locale.jsgetProvinceDef wrapper.
src/modules/expenses/expenses.jsgetAllCategories() merges provinceDef.expenseCategories.