Skip to content

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


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)”
StepRule
1If resolveProvinceDef(countryId, provinceId) has non-empty availablePlatforms, use it (province wins).
2Else if country row has defaultAvailablePlatforms (non-empty), use it.
3Else if ProvinceRegistry.getByCountry(countryId) is non-empty, union all their availablePlatforms.
4Else all ids in PlatformRegistry.

Platform ids stay lowercase end-to-end (Dexie seeds, defs, resolvers).

SourceRule
MapsOnly withholding-presets.js — do not duplicate in modules.
SubdivisionsOne *.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.
SelectorCountry tax.regionPresetType is 'CA', 'US', or null (UK has no map).
FallbackUS unknown code uses tax.defaultWithholdingPct; CA unknown should not occur if region matches catalog.

When reading “which market is this user?” use this order:

PriorityFieldRole
1user.countryIdCanonical ISO market (CA, US, UK). Prefer for registry resolution.
2user.provinceIdSubdivision; may be empty for some UK flows. Used with countryId in resolveProvinceDef.
3user.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.


ConceptStorage / persistenceUI / 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 grossEarningsCentsForms may show dollars; convert at save boundary.
DistanceShifts use distanceKm in storagelocale.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)”
FieldDEFAULT_USER (db.js)Registry / product intent
countryId / locale.countryCAMatches CA.country.js.
provinceIdONMatches CA/ON.province.js as default catalog market.
taxWithholdingPct29Matches 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.


LocationVerdict
CountryRegistry / ProvinceRegistry FALLBACK_IDIntentional global catalog default.
DEFAULT_USER, Dexie v3 migration shift backfillIntentional Ontario-first product default.
resolveProvinceDef('CA','') single-province returnIntentional for Canada-only catalog.
shifts.js resolveProvinceId final 'ON'Intentional last resort when user unset (documented; prefer user provinceId when present).
expenses.js same patternSame as shifts.
vehicles.js recurring expense provinceIdUpdated to use store.get('user') — CA → ON, else user provinceId or ''.
db.js logical migration provinceId for non-CAFixed 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.


Onboarding setup export (buildOnboardingSetupExport) includes:

FieldPurpose
exportKindAlways comma_setup for this file shape.
versionNumeric export schema version (currently 1). Bump when adding/removing top-level keys.
countryIdSame as draft country (stable market id).
provinceIdSame as draft taxRegion / completed provinceId (subdivision or free-text code).
platformsArray 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.marketContext: { countryId, provinceId, countryDef, provinceDef } from getMarketContext. Updates whenever user is set. Subscribe for UI that needs both defs without re-importing registries.
  • store.provinceDef may be null (e.g. US state not in ProvinceRegistry). Do not assume Ontario expenseCategories; 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.