3.5 KiB
geography — provinces, cities, districts
Client seam
client/src/services/geography/·USE_GEOGRAPHY_MOCK = false(real) · 13 server ops Last verified: 2026-07-30 against commitd3ec723and swagger.v1.json (2026-07-29).
The reference hierarchy every address, service area and search filter is keyed on. Also the home of the single most load-bearing null in the schema.
Endpoints
| Method | Path | Auth | Verdict |
|---|---|---|---|
| GET | /api/v1/geo/provinces |
anonymous | wired |
| GET | /api/v1/geo/cities |
anonymous | wired |
| GET | /api/v1/geo/districts |
anonymous | wired |
| GET | /api/v1/geo/tree |
anonymous | unwired — the client fetches the three levels separately and caches each |
| POST | /api/v1/admin_geo/create_province |
admin | unwired — no console screen |
| POST | /api/v1/admin_geo/update_province/{id} |
admin | unwired |
| POST | /api/v1/admin_geo/set_province_active/{id} |
admin | unwired |
| POST | /api/v1/admin_geo/create_city |
admin | unwired |
| POST | /api/v1/admin_geo/update_city/{id} |
admin | unwired |
| POST | /api/v1/admin_geo/set_city_active/{id} |
admin | unwired |
| POST | /api/v1/admin_geo/create_district |
admin | unwired |
| POST | /api/v1/admin_geo/update_district/{id} |
admin | unwired |
| POST | /api/v1/admin_geo/set_district_active/{id} |
admin | unwired |
No phantoms. The nine admin_geo routes are DynamicPermission + sensitive; geography is seeded, so
there is no console editor. Mutations are action-style (admin_geo/create_city, never
admin_geo/cities).
districtId = null means whole-city
This is the one rule to get right, and it reads in both directions:
- On a nurse service area (see service-areas.md),
districtId = nullmeans the nurse covers the entire city, not "no district". - On a search query, a customer in district D must match both a nurse whose area names D and a nurse whose area is whole-city. See search.md.
- On an address it is genuinely optional metadata — a missing district does not widen anything.
Never coerce the null to 0 or to a sentinel id, and never write a query that drops whole-city rows.
Shape rules the JSON does not express
- Every level returns both
nameFaandnameEn; the client picks by locale. isActivefilters the pickers. An inactive city must stay resolvable — an existing address or booking references it — so it is filtered from selection, never deleted.- Two Neshan keys exist and they are different products. The server's geocoder key is
Seams:Geocoding:ApiKey(server-side address→point); the client's map key isNEXT_PUBLIC_NESHAN_KEY(a web key for the embeddable map/search). Never share one value between them. With the client key unset,AddressMapPickerfalls back to a bounded-canvas grid, which is why dev, CI and jsdom all work without it. See ../config-matrix.md. - Geocoding is a seam:
Seams:Geocoding:Provider=mock(default) orneshan. The mock resolves a deterministic point near the city centroid; an address containingNO_GEOresolves to null coordinates so the "saved without a map pin" state is testable per-request.
Enums
None. All three levels are integer ids with localised names.
Open REQs
None. REQ-008 (accept the client-picked pin) and REQ-009 (provinceId on the address DTO) were delivered
and are documented in addresses.md.