Files
baya-monorepo/docs/flows/addresses-and-map.md
T
2026-08-02 17:18:36 +03:30

12 KiB

Flow — addresses and map

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer · Status: partial Client: real · Server: real Business source: product/business/01-actors-and-onboarding.md (customer_addresses) · pin purpose: 06-evv-and-service-delivery.md Integration: addresses.md · geography.md

What it does

The family's address book: where the nurse is asked to come. Each address is a province → city → (optional) district choice plus a free-text street line and a map pin; exactly one address is primary, and the primary one is preselected on the C4 booking-request form. The pin is what the nurse's EVV check-in is later measured against, so it is the reason the map exists at all.

Screens

Step Route Component / notes
1 /fa/addresses (customer)/addresses/page.tsx'use client', no generateMetadata. List / skeleton / ErrorState / EmptyState, add-edit FormDialogShell, delete ConfirmDialog
1a AddressCard.tsx — title, «شهر · منطقه», street line, «آدرس اصلی» badge, «پین ثبت شده» / «پین ندارد»
2 dialog AddressForm.tsx — react-hook-form. Required: title, city, pin, street line. District optional
2a dialog CascadingRegionSelect.tsx — three TextField selects; the empty district option is the explicit «کل شهر» choice (:152)
2b dialog AddressMapPicker.tsxbranches on NESHAN_WEB_KEY at :43. Key set → NeshanMap (Leaflet + Neshan tiles, search box, locate-me, reverse-geocoded preview). Key unset → GridFallbackMap (:78)
3 /fa/bookings/request C4 reads the book (page.tsx:116) and preselects the primary (:183-184). Read-only — no inline create; the empty state CTA routes to /fa/addresses (:383)
3a /fa/profile account-hub row «مدیریت آدرس‌ها» → /fa/addresses

What a tester actually sees on the map

client/.env.development does not define NEXT_PUBLIC_NESHAN_KEY (the variable appears only in .env.production, commented out, and in .env.sample). config.ts:25 therefore resolves NESHAN_WEB_KEY = undefined, so AddressMapPicker renders the grid stand-in: a 220 px --bal-primary-soft panel with a 28 px CSS grid, no tiles, no search box and no locate-me button. Tapping or dragging places a teardrop pin and prints «عرض» / «طول» to 5 decimals underneath. The canvas spans ±0.06° (~±6 km) around the chosen city's centroid (SPAN, :65). It is a real coordinate emitter, not a decoration — the value it produces is what create/update send.

API

Shapes belong to addresses.md and geography.md — not repeated here.

Call Endpoint Notes
useAddresses GET /api/v1/customer_addresses/list clientApi.ts:46ListMyAddressesQuery. Probed with T_09120000010: total: 1, provinceId: 1, districtId: 1003, pin 35.7595 / 51.41, postalCode/recipientName/recipientPhone all populated
useCreateAddress POST /api/v1/customer_addresses/create CreateAddressCommand.Handler.cs:50-62 — a sent pin wins (GeocodeSources.UserPin); only a missing pin calls IGeocoder
useUpdateAddress POST /api/v1/customer_addresses/update/{id} UpdateAddressCommand.Handler.cs:62-74 — pin wins; re-geocodes only when city/district/line changed
useSetPrimaryAddress POST /api/v1/customer_addresses/set_primary/{id} demote + promote in one transaction; the client invalidates the whole list key
useDeleteAddress DELETE /api/v1/customer_addresses/delete/{id} soft delete behind the global query filter
useProvinces / useCities / useDistricts GET /api/v1/geo/{provinces,cities,districts} anonymous, staleTime: Infinity. Probed: 31 provinces; province_id=1 → 1 city (101 تهران); city_id=101 → 22 districts
GET /api/v1/geo/tree live (probed: 200) but unwired — the client fetches the three levels separately
map search / reverse https://api.neshan.org/v1/search, /v5/reverse geography/neshan.ts — a deliberate direct third-party fetch, bypassing clientFetch so our bearer is never sent to Neshan (:8-9). Both return null immediately while the key is unset (:29)

Server seam: IGeocoder = MockGeocoder (no Seams:Geocoding:Provider is set anywhere) — deterministic jitter near the city centroid; an address line containing NO_GEO resolves to null coordinates.

Rules that must hold

Rule Source
districtId = null on an address is optional metadata — it widens nothing. The same null on a nurse service area means whole-city (INV-3). Never coerce it to 0. geography.md · nurse-service-areas.md
The client-picked pin is authoritative — the server does not re-geocode over it (REQ-008, delivered). addresses.md
latitude/longitude are nullable — null means "saved without a map pin", a display state, never an error. addresses.md
address_snapshot_json freezes the address onto the booking — editing or deleting an address never rewrites history. product/business/05 §Snapshots
A booking request sees a city/district-coarse mask, not the line — two-stage disclosure (INV-6). booking-request.md
The pin feeds the EVV check-in distance test at evv_location_tolerance_meters = 200 m, advisory only — a mismatch raises a support alert, never blocks or cancels. product/business/06
addressLine/postalCode/recipientName/recipientPhone are encrypted at rest and decrypted only for the owner (INV-21). product/business/01
Exactly one primary; the first address saved is forced primary (Handler.cs:42-43). server handler

How to test

  1. Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md.
  2. Go to /fa/profile → «مدیریت آدرس‌ها», or straight to /fa/addresses. Expect: exactly one card — «خانه», «تهران · منطقه ۳», the ملاصدرا street line, the «آدرس اصلی» badge and «پین ثبت شده».
  3. Tap «افزودن آدرس». Pick استان تهران → شهر تهران. Expect: the city select holds exactly one option; the district select then offers «کل شهر» plus 22 «منطقه ۱…۲۲» rows.
  4. Submit with the pin untouched. Expect: the form blocks with «روی نقشه یک پین بگذارید» — the pin is a required field (AddressForm.tsx:131).
  5. Tap anywhere on the grid panel, then Save. Expect: a «آدرس ذخیره شد» toast, the list refetches, and the new card shows «پین ثبت شده». Confirm server-side: curl -s --noproxy '*' "http://localhost:5002/api/v1/customer_addresses/list?page=1&pageSize=10" -H "Authorization: Bearer $T_09120000010"total: 2 and non-null latitude/longitude on the new row.
  6. Toggle «انتخاب به‌عنوان آدرس اصلی» on the new card. Expect: «آدرس اصلی به‌روزرسانی شد» and the badge moves — never two badges.
  7. Delete the address you just created. Expect: «آدرس حذف شد» and total back to 1.
  8. Pick استان اصفهان instead in step 3. Expect (and this is the defect): the map panel re-centres on the Iran centroid (32.4279, 53.688 — empty desert), not on Isfahan, because real Isfahan is city id 103 while CITY_CENTROIDS only knows 301 (see gaps).

Do not edit the seeded address id 1 — the edit dialog silently wipes its postal code and recipient fields (see gaps). Create a throwaway address and edit that instead. The world is shared and 7 days stale, but nothing in this flow depends on the aged booking data.

Known gaps

  • NEXT_PUBLIC_NESHAN_KEY is unset in client/.env.development and commented out in .env.production, so NeshanMap never renders anywhere today — every environment gets the keyless grid stand-in. The real map path (Leaflet, tiles, address search, locate-me, reverse-geocode preview) is entirely unexercised.
  • NESHAN_TILE_URL_TEMPLATE (geography/constants.ts:51) and every response field name in geography/neshan.ts (items[].location.x/y, formatted_address) are UNVERIFIED against the live Neshan API — both files say so in their own headers. Nobody has ever run this code against platform.neshan.org. Setting a key may yield a blank tile layer and a silently empty search box.
  • CITY_CENTROIDS (geography/constants.ts:27-36) is keyed on the mock seed's city ids (201 Mashhad, 301 Isfahan, 401 Shiraz, …). Live probe: real cities are 101+provinceId-adjacent — Tehran 101, Karaj 102, Isfahan 103. Only Tehran matches. For every other city cityCentroid() falls back to the Iran centroid, so the picker opens ~400 km from the customer and the ±6 km canvas can never reach their street.
  • Editing an address through the UI destroys data. AddressForm.submit (:91-102) never collects postalCode, recipientName or recipientPhone; clientApi.toBody (:21-34) coerces the missing values to null; UpdateAddressCommand.Handler.cs:57-59 assigns them unconditionally. Editing seeded address 1 nulls 1991834511 / «بهرام محمدی» / 09121110010. Silent, no warning, not recoverable from the UI.
  • Those same three fields are never displayed anywhere either — AddressCard shows title/region/line only, so a customer cannot see or set a recipient name for a visit that is not for themselves.
  • The pin is a required form field, so the client can never take the create/update path that has no pin — IGeocoder is unreachable from the web app, and the latitude == null state (and AddressCard's «پین ندارد» badge) is unreachable except via a direct API call or seeded data.
  • Geography is seeded one city per province (31 provinces, 31 cities; probed province_id 2/3/5 → 1 city each) and districts exist only for Tehran (city_id=103 → 0). Outside Tehran the cascade is a two-step formality and «کل شهر» is the only district choice.
  • GET /api/v1/geo/tree is live and returns 200 but no client consumes it; the cascade makes three round trips instead of one.
  • AddressMapPicker's pin geometry is applied via inline style specifically to dodge the RTL stylis plugin — correct, but fragile and untested for RTL drift; AddressMapPicker.test.tsx only asserts the keyless fallback path.
  • No product/ doc describes address entry, the map-pin picker, or the Neshan integration. business/01 merely lists the customer_addresses table; geography is documented supply-side only (business/04 service areas). This flow's UI rules have no business source to check against.