Adding a province or territory
Provinces — and by the same pattern **states / regions** — are static catalog data attached to a country.
- 1PhoneAdd src/registry/countries/{ISO}/provinces/{CODE}.ts and list it in the country's _PROVINCES.
- 2WebAdd web/src/registry/provinces/{ISO}/{CODE}.province.js, then rebuild the generated index.
- 3Gatenpx tsc --noEmit and node scripts/check-country-parity.mjs must both pass.
Currently registered: Canada provinces only. US state stubs were removed with the US country pending accurate tax research. Use Ontario (ON) as your reference on both sides. Add the country first (adding-a-country.md) before any province that references its countryId.
Part A — Phone app (TypeScript)
On the phone, provinces live inside their country's folder and are collected into that country's _PROVINCES array.
- Create
src/registry/countries/{ISO}/provinces/{CODE}.ts(short uppercase subdivision code, e.g.BC.ts). CopyCA/provinces/ON.tsfor the shape. export const {CODE}: ProvinceDef. Required by the type:id(uppercase),label,countryId(matches the country'sid),salesTaxRate,isHarmonized. Optional:withholdingPct,usesPensionPlan,bannedPlatforms,secondarySalesTaxRate, and the other fields intypes.ts.- Add it to the country's list in
{ISO}/index.ts:import { {CODE} } from "./provinces/{CODE}"then include{CODE}in{ISO}_PROVINCES. - That array is spread into
ALL_REGIONSinsrc/registry/countries/index.tsat registration time (see the country guide), which is whatresolveProvinceDef/getRegionsByCountryread. npx tsc --noEmitpasses.
Part B — Web app (JavaScript)
On the web, provinces live under web/src/registry/provinces/{ISO}/ and index.js is generated by a script.
- Create folder
web/src/registry/provinces/{ISO}/if it does not exist (two-letter country id). - Copy
web/src/registry/provinces/CA/_TEMPLATE.province.jstoweb/src/registry/provinces/{ISO}/{CODE}.province.js. Use a short uppercaseidinside the file. - Set
countryIdto a country that already exists in the webCountryRegistry. - Fill
labelKey(e.g.provinces.bc) and add the same path understrings.enandstrings.frinweb/src/utils/strings.js— mirror every key in both locales. - Set
availablePlatformsto an array ofPlatformRegistryids (same strings as the platform files'id). Only listed platforms count as "available" for that market. - Define
expenseCategories(recommended): each row needs a stableid, an i18nlabelKey(usually underexpenses.cat), and optionalcraLine,mixedUse,vehicleTypes. These feedgetAllCategories(). - Optional blocks:
salesTax,incomeTax,pensionContribution,vehicleExpenseMethod,referenceUrl,vehicleNotes,onboardingExtras— mirrorCA/ON.province.js. - If the region has a withholding hint, add it to the country's table in
withholding-presets.js. - Regenerate the index (see below), then
npm run build(fromweb/) and ensureassertProvinceRegistryValidpasses at boot.
Regenerate the web index
Province modules live under web/src/registry/provinces/{ISO}/. After you add or remove a *.province.js, regenerate the index (run from web/):
npm run rebuild:provincesThis rewrites web/src/registry/provinces/index.js: it scans every two-letter country folder and imports each *.province.js except names starting with _ (templates). It is country-agnostic — no per-country special cases. Canada keeps Ontario first (it is the fallback), then A–Z. Any other country's folder is picked up automatically once it exists.
Registry rules (enforced)
validateProvinceDefinition on the web requires:
| Field | Rule |
|---|---|
id | Present; looked up with .toUpperCase() — keep ids uppercase. |
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. An empty array passes the validator but is useless to drivers. |
Unknown ids: ProvinceRegistry.getById(x) falls back to FALLBACK_ID (ON). If your primary market is no longer Ontario, change that fallback deliberately — it affects every unresolved provinceId.
Wiring outside the registry
| Area | What to do |
|---|---|
| User default | DEFAULT_USER / migrations (web) and phone store defaults — if a new province should be the default, set provinceId (and countryId / locale) consistently. |
| Store | Web syncLocaleDefsFromUser already loads provinceDef — no change if only catalog data was added. |
| Onboarding | v3 onboarding is Ontario-oriented (steps.js). Multi-region onboarding means new steps / selectors — not automatic from the def alone. |
| Tax module | Province-aware summaries may still need updates in tax.js for a new tax regime. |
QA
- Parity:
node scripts/check-country-parity.mjsgreen;npx tsc --noEmitclean. - Set
provinceIdto the new code on each app and reload: the activeprovinceDef.idmatches. - Open Expenses then add an expense: the category grid prefers the province
expenseCategorieslist.
Related files
| File | Role |
|---|---|
src/registry/countries/CA/provinces/ON.ts | Phone reference province. |
src/registry/countries/types.ts | Phone ProvinceDef type. |
web/src/registry/provinces/CA/_TEMPLATE.province.js | Web starter def. |
web/src/registry/provinces/CA/ON.province.js | Web full reference implementation. |
web/src/registry/provinces/index.js | Web registry + validation + fallback (generated). |
web/scripts/rebuild-province-index.js | Regenerates the web index from the folders. |
See also
- adding-a-country.md — add the country (both apps) before provinces that reference
countryId. - adding-a-platform.md — add a platform before listing it in
availablePlatforms.