Files
baya-monorepo/archive/build-chain/working-context/backend/handoff/after-backend-phase-4.md
T
2026-08-02 18:48:32 +03:30

4.4 KiB

After backend-phase-4 — geography, addresses & nurse service areas are live

The geographic spine the marketplace stands on now exists. There is a real province→city→district hierarchy (tables, not code lists), nurses can declare where they travel, and customers can save encrypted, geocoded service addresses. Contract: dev/contracts/domains/geography-addresses.md; machine schema: dev/contracts/openapi/swagger.v1.json (refreshed).

What the frontend (f3-b4) can now build

  • Cascading province/city/district dropdownsGET api/v1/geo/provinces, …/geo/cities?province_id=, …/geo/districts?city_id= (each active-only, ordered by sortOrder), or the whole active tree in one call via GET api/v1/geo/tree. An empty district list is normal — that city is whole-city-only; let the user pick "whole city".
  • Nurse coverage-area editorPOST api/v1/nurse_service_areas/add { cityId, districtId? } (omit districtId = whole city), DELETE …/nurse_service_areas/remove/{id}, GET …/nurse_service_areas/list. Each row carries an isWholeCity flag. Requires a nurse profile first (b3 nurse_profiles/upsert).
  • Address book + map-pin pickerPOST api/v1/customer_addresses/create { title, cityId, districtId?, addressLine, postalCode?, recipientName?, recipientPhone?, isPrimary? }, update/{id}, set_primary/{id}, delete/{id}, list (primary first). The create/update response and the list return latitude/longitude for the map pin (nullable — render a "pin not set" state when null), and the address is decrypted for the owner.

Rules baked into the API (don't fight them client-side)

  • districtId = null means "the entire city" — a deliberate coverage choice, not "unset". Show it as a first-class option; a whole-city service area matches every district in that city when search lands (b7).
  • Duplicate service area → 409 (including a duplicate whole-city row), never a 500. Surface it as "you already cover this".
  • Single primary address — the first address is primary automatically; setting another primary clears the previous one. There is always exactly one.
  • is_active hides, never deletes — a deactivated region simply drops out of the dropdowns.
  • Address PII is encrypted at rest and only ever returned to the owning customer. Coordinates and the title label are not PII.
  • Tenancy — service areas and addresses are strictly owner-scoped; another owner's id returns 404.
  • Refresh after select_role still applies (nurse/customer scoping reads the role claim in the token).
  • Routes are action-style (admin_geo/create_city, nurse_service_areas/add, customer_addresses/create, …) — see the contract for the full list.

What's mocked

  • Geocoding (IGeocoder🟡). MockGeocoder returns deterministic coordinates around the city centroid with no network call. A config switch (Seams:Geocoding:ReturnNullCoordinates) or a NO_GEO marker in the address text forces the null-coordinate path so the "saved without a map pin" UI state is testable. Real Neshan/Google geocoding is a drop-in registration swap (see mocks-registry).

Schema / migration

Migration 20260702093332_GeographyAddressesServiceAreas (applies on startup): geo.Provinces, geo.Cities, geo.Districts, geo.NurseServiceAreas, usr.CustomerAddresses. Whole-city uniqueness is a filtered-index pair (UNIQUE(nurse_id, city_id) WHERE district_id IS NULL AND deleted_at IS NULL + UNIQUE(nurse_id, city_id, district_id) WHERE district_id IS NOT NULL AND deleted_at IS NULL); addresses carry a filtered UNIQUE(customer_id) WHERE is_primary=1 AND deleted_at IS NULL; coordinates are decimal(9,6); address PII columns are encrypted. Seed: 31 provinces (Tehran id 1), capital cities (Tehran city id 101), Tehran districts 1001…1022.

Deferred to later phases (do not build against these yet)

  • nurse_search_index fan-out on service-area add/remove → b7 (the add/remove handlers are the clean trigger point).
  • GPS-radius / "nurses near me" map discovery → not planned; coverage is named districts, full stop.
  • EVV distance check that consumes the address latitude/longitudeb9 (this phase only produces the coordinates).
  • Region bulk-import feed (IGeoDataImporter) → deferred; the idempotent seed + admin CRUD is enough for MVP.