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 store → provinceDef 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.
Checklist
Section titled “Checklist”- Create folder
src/registry/provinces/{ISO}/if it does not exist (two-letter country id, same asCountryRegistry). - Copy
src/registry/provinces/CA/_TEMPLATE.province.jstosrc/registry/provinces/{ISO}/{CODE}.province.js(e.g.CA/BC.province.js,UK/ENG.province.js). For US states, addUS/{CODE}.province.jsusing the one-line pattern in an existing state file (seeUS/_usStateProvince.js+ e.g.US/TX.province.js). Use a short uppercaseidinside the file for the subdivision code. - Set
countryIdto a country that already exists inCountryRegistry(CA,US, …). - Fill
labelKey(e.g.provinces.bc) and add the same path understrings.enandstrings.frinsrc/utils/strings.js—t()walks dot segments, soprovinces.bcmaps to nestedprovinces → bcstring leaves (mirror every key in both locales). - Set
availablePlatformsto an array ofPlatformRegistryids (same strings asdoordash.platform.jsid). Only listed platforms are treated as “available” for that market when building picker / province-driven UX. - Define
expenseCategories(recommended): each row needs stableid, i18nlabelKey(usually underexpenses.cat.*— seeCA/ON.province.js), and optionalcraLine,mixedUse,vehicleTypesfor driver guidance. These feedgetAllCategories()whenstore.get('provinceDef')is set. - Optional blocks:
salesTax,incomeTax,pensionContribution,vehicleExpenseMethod,referenceUrl,vehicleNotes,onboardingExtras— mirror the shape used inCA/ON.province.jswhere applicable. - Run
npm run rebuild:provincessoindex.jsimports the new file (all country folders are scanned). For US states using the shared factory, also add a row towithholding-presets.jswhen a withholding hint applies. - Run
node build.js --prodand ensure startup validation passes (assertProvinceRegistryValidinmain.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:
npm run rebuild:provincesThis 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:
node scripts/rebuild-province-index.js --force-usThen run npm run build as usual.
Registry rules (enforced)
Section titled “Registry rules (enforced)”validateProvinceDefinition requires:
| Field | Rule |
|---|---|
id | Present; looked up with .toUpperCase() — keep ids uppercase in the file. |
countryId | Present; must match how you filter with ProvinceRegistry.getByCountry('CA'). |
availablePlatforms | Non-empty array of platform id strings. |
expenseCategories | Required 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).
Field reference (practical)
Section titled “Field reference (practical)”labelKey
Section titled “labelKey”String passed to t(). Add nested keys in strings.js for both locales so UI does not show raw key paths.
availablePlatforms
Section titled “availablePlatforms”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.
expenseCategories
Section titled “expenseCategories”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.
salesTax / incomeTax
Section titled “salesTax / incomeTax”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.
onboardingExtras
Section titled “onboardingExtras”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.
vehicleNotes / referenceUrl
Section titled “vehicleNotes / referenceUrl”Optional strings for province-specific copy keys and CRA/help links.
Wiring outside the registry
Section titled “Wiring outside the registry”| Area | What to do |
|---|---|
| User default | DEFAULT_USER / migrations — if a new province should be the default for new vaults, set provinceId (and countryId / locale) consistently. |
| Store | syncLocaleDefsFromUser already loads provinceDef from getProvinceDef(user.provinceId) — no change needed if only catalog data was added. |
| Onboarding | Current 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 module | Province-aware summaries read country + user + expense data; heavy province logic may still need updates in tax.js for new regimes. |
Minimal example skeleton
Section titled “Minimal example skeleton”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.provinceIdto the new code (Settings or Dexie) and reload:store.provinceDef.idshould match. - Open Expenses → add: category grid should prefer the province
expenseCategorieslist. - Grep for hardcoded
'ON'outside migrations/fallbacks; replace withprovinceDeforuser.provinceIdwhere appropriate when you truly support multiple provinces.
Related files
Section titled “Related files”| File | Role |
|---|---|
src/registry/provinces/CA/_TEMPLATE.province.js | Empty-ish starter def (Canada). |
src/registry/provinces/CA/ON.province.js | Full reference implementation (Canada). |
src/registry/provinces/US/_usStateProvince.js | Shared factory for default US state rows. |
src/registry/provinces/index.js | Registry + validation + fallback. |
src/utils/locale.js | getProvinceDef wrapper. |
src/modules/expenses/expenses.js | getAllCategories() merges provinceDef.expenseCategories. |
See also
Section titled “See also”- Adding a Platform — add a platform before listing it in
availablePlatforms. - Adding a Country — add the country before provinces that reference
countryId.