Market Resolution
Single narrative for country-first, province-override market data, shared tax presets, and cross-cutting rules (identity, money, exports, store, audits).
Code entrypoints
src/registry/market/resolve.js—getMarketContext,resolveAvailablePlatformIds.src/registry/tax/withholding-presets.js—WITHHOLDING_PRESETS_CA/WITHHOLDING_PRESETS_US,getWithholdingPresetPct,listUsWithholdingRegionCodes(US ids mirrorProvinceRegistry.getByCountry('US')).src/registry/provinces/—ProvinceRegistry:{ISO}/*.province.js(one folder per two-letter country:CA/,US/,UK/, …). Runnpm run rebuild:provincesto refreshindex.jsfrom those folders. US states default throughUS/_usStateProvince.js; keepwithholding-presets.jsWITHHOLDING_PRESETS_USin sync when adding a US code.store.marketContext— same shape asgetMarketContext(...)after each user sync (seestore.js).
Merge model (overview)
Section titled “Merge model (overview)”flowchart TD
subgraph inputs [Inputs]
countryId[countryId]
provinceId[provinceId]
end
subgraph catalogs [Static catalogs]
countryRow[CountryRegistry row]
provinceRow[ProvinceRegistry row or null]
end
subgraph resolver [Resolver API]
merge[mergeCountryProvince per field rules]
end
inputs --> countryRow
inputs --> provinceRow
countryRow --> merge
provinceRow --> merge
merge --> consumers[Onboarding Tax Expenses Shifts Notifications]
Platform allow-list (encoded in resolveAvailablePlatformIds)
Section titled “Platform allow-list (encoded in resolveAvailablePlatformIds)”| Step | Rule |
|---|---|
| 1 | If resolveProvinceDef(countryId, provinceId) has non-empty availablePlatforms, use it (province wins). |
| 2 | Else if country row has defaultAvailablePlatforms (non-empty), use it. |
| 3 | Else if ProvinceRegistry.getByCountry(countryId) is non-empty, union all their availablePlatforms. |
| 4 | Else all ids in PlatformRegistry. |
Platform ids stay lowercase end-to-end (Dexie seeds, defs, resolvers).
Withholding presets
Section titled “Withholding presets”| Source | Rule |
|---|---|
| Maps | Only withholding-presets.js — do not duplicate in modules. |
| Subdivisions | One *.province.js per region under provinces/{ISO}/ (see CA/ON, US/ + _usStateProvince.js). Refresh index.js with npm run rebuild:provinces. US: also add WITHHOLDING_PRESETS_US when withholding applies. |
| Selector | Country tax.regionPresetType is 'CA', 'US', or null (UK has no map). |
| Fallback | US unknown code uses tax.defaultWithholdingPct; CA unknown should not occur if region matches catalog. |
Market identity (precedence)
Section titled “Market identity (precedence)”When reading “which market is this user?” use this order:
| Priority | Field | Role |
|---|---|---|
| 1 | user.countryId | Canonical ISO market (CA, US, UK). Prefer for registry resolution. |
| 2 | user.provinceId | Subdivision; may be empty for some UK flows. Used with countryId in resolveProvinceDef. |
| 3 | user.locale.country (and currency / distanceUnit) | Display and Intl; should match countryId after onboarding and migrations. If conflict during imports, normalize toward countryId + getLocaleConfig(countryId). |
Store / sync: syncLocaleDefsFromUser resolves countryId from user.countryId first, then locale.country, then 'CA' only as last-resort default. provinceId uses the stored value when non-empty; for legacy Canada-only rows with missing provinceId, default ON only when countryId === 'CA'. Non-CA users must not silently get ON.
Money and units (storage vs UI)
Section titled “Money and units (storage vs UI)”| Concept | Storage / persistence | UI / draft |
|---|---|---|
Goals (weeklyGoal, etc. on user) | Integer cents (plan v3) | Onboarding draft often uses dollar integers until saveUser multiplies by 100 — see onboarding.js buildCompletedUserPatch. |
Shift grossEarnings | Cents | Forms may show dollars; convert at save boundary. |
| Distance | Shifts use distanceKm in storage | locale.distanceUnit is 'km' | 'mi' for display only — do not infer country from km alone. |
Canonical conversion helpers live next to the modules that own the forms (shift form, onboarding, goals), not inside aggregations.
Dexie DEFAULT_USER vs registry (alignment)
Section titled “Dexie DEFAULT_USER vs registry (alignment)”| Field | DEFAULT_USER (db.js) | Registry / product intent |
|---|---|---|
countryId / locale.country | CA | Matches CA.country.js. |
provinceId | ON | Matches CA/ON.province.js as default catalog market. |
taxWithholdingPct | 29 | Matches WITHHOLDING_PRESETS_CA.ON in withholding-presets.js (not tax.defaultWithholdingPct 28 — preset row is the seed for set-aside UI). |
Logical migration schema 1 backfills countryId / provinceId; non-CA users must not receive ON as provinceId when absent — use '' except Canada → ON.
CA / ON grep triage (audit backlog)
Section titled “CA / ON grep triage (audit backlog)”| Location | Verdict |
|---|---|
CountryRegistry / ProvinceRegistry FALLBACK_ID | Intentional global catalog default. |
DEFAULT_USER, Dexie v3 migration shift backfill | Intentional Ontario-first product default. |
resolveProvinceDef('CA','') single-province return | Intentional for Canada-only catalog. |
shifts.js resolveProvinceId final 'ON' | Intentional last resort when user unset (documented; prefer user provinceId when present). |
expenses.js same pattern | Same as shifts. |
vehicles.js recurring expense provinceId | Updated to use store.get('user') — CA → ON, else user provinceId or ''. |
db.js logical migration provinceId for non-CA | Fixed to avoid assigning ON to US/UK users. |
Re-run rg "'CA'|'ON'" src after major features; new hits should be classified the same way.
Exports and portability (setup JSON)
Section titled “Exports and portability (setup JSON)”Onboarding setup export (buildOnboardingSetupExport) includes:
| Field | Purpose |
|---|---|
exportKind | Always comma_setup for this file shape. |
version | Numeric export schema version (currently 1). Bump when adding/removing top-level keys. |
countryId | Same as draft country (stable market id). |
provinceId | Same as draft taxRegion / completed provinceId (subdivision or free-text code). |
platforms | Array of platform id strings (lowercase). |
Full vault backups use Dexie + separate processes; keep platform / country / province ids stable when renaming catalog entries (add migration mapping if ids ever change).
Store and events
Section titled “Store and events”store.marketContext:{ countryId, provinceId, countryDef, provinceDef }fromgetMarketContext. Updates wheneveruseris set. Subscribe for UI that needs both defs without re-importing registries.store.provinceDefmay benull(e.g. US state not inProvinceRegistry). Do not assume OntarioexpenseCategories;getAllCategories()already merges globals when province data is missing.- Bus payloads: include
source(string) on app-emitted events so listeners can avoid feedback loops (sample,onboarding, etc.).
Adding a province that overrides country defaults
Section titled “Adding a province that overrides country defaults”When a new *.province.js exists with availablePlatforms, resolvers prefer it over country.defaultAvailablePlatforms. No separate “exempt list” in code.
Related docs
Section titled “Related docs”- Adding a Country, Adding a Province, Adding a Platform
- Feature Modularity (Category A — link from registry section)
- Registry Architecture