regions-and-countriesrun.md02_define/output/spec.mdLocation is recorded at a territory and state level of detail the business does not use, so a regional total has to be assembled by hand and cannot be trusted to add up. Paul's rule is blunt: two geographic levels only, used consistently for users, customers, service leads, dashboards and reporting (FR-2, FR-10). This advances Q2-2026 Objective 1 (Establish Product-Market Fit with Vendor Partners): regional reporting that adds up is what the Refine-the-bridge initiative's partner conversations lean on.
Region and Country become the only two location levels anywhere in the platform.
packages/services (/shared), with no external dependency. Countries are
chosen from it, never typed free-hand (BR-15).location collection becomes region → countries. territories[] is replaced by
countries[], each validated against the standard list. Within an organisation a country
belongs to at most one region (BR-16) — create/update refuses a country already grouped
elsewhere, so regional totals add up without double counting.location.region strings on the tenant's users and leads in the same operation.validateRegionCountry replaces
validateRegionTerritory; the user-profile inputs become pickers (region from the
organisation's regions, country from the standard list — narrowing to the region's countries
when one is chosen, and clearing a country that no longer belongs). A country not yet grouped
into any region is a legitimate state and can still be recorded (it simply has no region).territory/state (a migration unsets stored values), the
admin "Locations" tab manages regions and countries instead of territories, and the remaining
territory reads (lead header, user profile, lead import, proposal summary, the expert-matching
prompt line) fall back to country.team-region-country-filters.03_build/output/notes.mdpackages/services/src/shared/countries.ts (new): ISO 3166-1 English short
names as a static list (D-8, no external dependency) with canonicalCountry
/ isKnownCountry; exported via /shared.packages/services/src/db/models/location.ts: the tenant region row is now
region → countries[] (was territories[]).packages/services/src/db/services/location/index.ts: countries are
canonicalised against the standard list (BR-15); a country may belong to at
most one region per tenant (BR-16 — create/update refuse with the clashing
region named); renaming a region cascades to the denormalised
location.region strings on users and leads in the same operation; delete
is refused while the region still holds countries or people, with the
reason.packages/services/src/utils/validators.ts (+ tests):
validateRegionCountry replaces validateRegionTerritory — country must be
on the standard list; when both are set the country must belong to the
region (BR-17); an ungrouped country with no region is valid.user.ts / lead.ts models: location.territory and location.state
removed — Region and Country are the only two levels. Street/zip stay as
contact info (D-13).1787616000000-retire-territory-and-state.ts: unsets stored
territory/state values on users and leads; swaps the locations
collection's territories[] for an empty countries[] (territory names are
not countries — admins group countries afterwards).matching/score-expert-fit.ts: the prompt's territory lines now read
country — no other matching change (whole-scope out-of-scope).apps/web: the admin Locations tab now manages regions of countries
(components/admin/regions-list/ replaces territories-list/; actions
moved to app/(app)/admin/settings/locations/actions.ts); the user
profile's region/country are pickers (region from the org's regions,
country narrowed to the region's countries, region change clears a country
that no longer belongs); updateTenantUser enforces BR-15/BR-17
server-side; the lead header, proposal detail and lead import lose
territory (import gains a Country column dependent on Region). The legacy
/admin/territories redirect page stays for bookmarks.validateRegionCountry and the profile picker.down restores shape, not retired values — stated in its
header; backups are the record.LocationService.softDelete keeps its name (it still deactivates rather
than removing the row) but now carries the delete guards; the UI copy says
"Delete".user-record-and-reporting-line — this PR's diff is against
that branch.