Adds the province -> city -> district reference hierarchy (geo schema, seeded with 31 provinces + capital cities + Tehran's 22 districts), nurse service areas (district_id NULL = whole city, filtered-index-pair uniqueness -> 409), and encrypted, geocoded customer addresses with a single-primary invariant. Introduces the IGeocoder seam (mocked) and 409 Conflict on the result envelope. Public cascading lookups are cached behind a generation-token scheme with invalidate-on-admin-write. One EF migration (GeographyAddressesServiceAreas, applied). Contract + swagger snapshot + handoff/report/registry updated. 103 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1.9 KiB
Domain 2 — Geographic Data
provinces / cities / districts [CORE]/[MVP]
Role: The geo hierarchy backing service areas, addresses, and search. Why a table, not a static list: new cities/districts launch without a deploy, and sort_order/is_active drive ordered, toggleable dropdowns. districts map to Tehran's 22 municipal districts or major neighborhoods elsewhere; they are optional (a nurse can cover a whole city). Fields unchanged. Relations: provinces 1:N cities 1:N districts; referenced by customer_addresses and nurse_service_areas.
nurse_service_areas [CORE]
Role: Where a nurse will travel. A row with district_id = NULL means the entire city. Why a join table (not a radius): Iranian nurses think in named districts, not GPS radii; this also drives the geographic filter in search cheaply. Unchanged, with UNIQUE(nurse_id, city_id, district_id). Relations: N:1 → nurse_profiles, cities, districts.
As-built (backend-phase-4): the geo hierarchy lives in a
geoschema (Provinces/Cities/Districts/NurseServiceAreas), seeded viaHasDatawith all 31 provinces, each province's capital city, and Tehran's 22 مناطق. Because SQL Server treats NULLs as distinct in a unique index, the whole-city (district_id = NULL) uniqueness is enforced with a filtered-index pair —UNIQUE(nurse_id, city_id) WHERE district_id IS NULL AND deleted_at IS NULLplusUNIQUE(nurse_id, city_id, district_id) WHERE district_id IS NOT NULL AND deleted_at IS NULL— so both a duplicate whole-city row and a duplicate city+district row are rejected (surfaced as409), while a soft-removed area can be re-declared. Public lookups are cached and filteris_activeat every level (a deactivated parent hides its children); geocoding is behind the mockedIGeocoderseam.