backend phase 4: geography, addresses & nurse service areas

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>
This commit is contained in:
hamid
2026-07-02 16:06:12 +03:30
parent 39a979b1a7
commit 82561c4cc6
113 changed files with 9817 additions and 5 deletions
+2
View File
@@ -21,6 +21,8 @@
<p><strong>Role:</strong> The geo hierarchy backing service areas, addresses, and search. <strong>Why a table, not a static list:</strong> new cities/districts launch without a deploy, and <code>sort_order</code>/<code>is_active</code> drive ordered, toggleable dropdowns. <code>districts</code> map to Tehran's 22 municipal districts or major neighborhoods elsewhere; they are <strong>optional</strong> (a nurse can cover a whole city). Fields unchanged. <strong>Relations:</strong> <code>provinces</code> 1:N <code>cities</code> 1:N <code>districts</code>; referenced by <code>customer_addresses</code> and <code>nurse_service_areas</code>.</p>
<h3 id="nurse_service_areas-core"><code>nurse_service_areas</code> [CORE] <a class="anchor" href="#nurse_service_areas-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Where a nurse will travel. A row with <code>district_id = NULL</code> means the entire city. <strong>Why a join table (not a radius):</strong> Iranian nurses think in named districts, not GPS radii; this also drives the geographic filter in search cheaply. Unchanged, with <code>UNIQUE(nurse_id, city_id, district_id)</code>. <strong>Relations:</strong> N:1 → <code>nurse_profiles</code>, <code>cities</code>, <code>districts</code>.</p>
<blockquote><p><strong>As-built (backend-phase-4):</strong> the geo hierarchy lives in a <strong><code>geo</code> schema</strong> (<code>Provinces</code>/<code>Cities</code>/ <code>Districts</code>/<code>NurseServiceAreas</code>), seeded via <code>HasData</code> with 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 (<code>district_id = NULL</code>) uniqueness is enforced with a <strong>filtered-index pair</strong><code>UNIQUE(nurse_id, city_id) WHERE district_id IS NULL AND deleted_at IS NULL</code> <strong>plus</strong> <code>UNIQUE(nurse_id, city_id, district_id) WHERE district_id IS NOT NULL AND deleted_at IS NULL</code> — so both a duplicate whole-city row and a duplicate city+district row are rejected (surfaced as <code>409</code>), while a soft-removed area can be re-declared. Public lookups are cached and filter <code>is_active</code> at every level (a deactivated parent hides its children); geocoding is behind the mocked <strong><code>IGeocoder</code></strong> seam.</p>
</blockquote>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>