backend phase 7: search & matching (nurse_search_index)

Add the discovery layer: the denormalized nurse_search_index read model
(one row per bookable variant x covered service area), maintained inline
inside each source write's transaction, plus the single public search
query behind the INurseSearch seam.

- Entity + EF config + migration (search schema): covering search index,
  filtered-unique (variant_id, city_id, district_id) pair with NULL
  district participating, nurse_id index, soft-delete.
- ISearchIndexMaintainer (write seam) + SearchIndexMaintainer: reindex
  variant / nurse / fan-out / remove-area / full rebuild, staged in the
  owning source write's unit of work; wired into the b3/b4/b5/b6 handlers.
- INurseSearch (read seam) + SqlNurseSearch (real MVP backend): reads only
  is_searchable=1, category/city/district(NULL-aware)/gender/price filters,
  rating sort, pagination. Elasticsearch deferred (config Search:Backend).
- SearchNursesQuery (+ validator) and RebuildSearchIndexCommand; public
  SearchController (GET search/nurses) + admin AdminSearchController
  (POST admin_search/rebuild_index).
- Tests: 9 DB-backed maintainer/search + 4 WebApplicationFactory; updated
  affected b3/b4/b5/b6 handler tests. Build clean, 167 tests green.
- Docs: server CLAUDE.md project map, contract search.md, swagger refresh,
  handoff, report, mocks-registry rows, STATUS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-05 17:24:26 +03:30
parent e2b22df2d6
commit 5839b3508f
47 changed files with 5743 additions and 41 deletions
@@ -12,6 +12,31 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
- **Notes for frontend:** <anything load-bearing>
-->
## backend-phase-7 — Search & matching (nurse search index) — 2026-07-05
- **Shipped:** the discovery layer via one additive migration — new **`search`** schema, **1 table**
`NurseSearchIndices` (the denormalized `nurse_search_index`): **one flat row per (bookable variant ×
covered area)** with copied category/price/unit, `city_id`/`district_id` (NULL = whole city), `nurse_gender`
+ rating aggregates, and the single **`is_searchable`** gate. It is a **read-only projection**, maintained
**inline in each source write's own transaction** by **`ISearchIndexMaintainer`** (`SearchIndexMaintainer`)
wired into the b3/b4/b5/b6 handlers (`ReindexVariant`/`ReindexNurse`/`FanOutServiceArea`/
`RemoveServiceAreaRows` + `Rebuild`). Read side is the **`INurseSearch`** seam — real MVP impl
`SqlNurseSearch` (reads only `is_searchable=1`; category/city/district(NULL-aware)/gender/price filters +
rating sort + pagination). **2 controllers:** public `SearchController` (`GET search/nurses`) + admin
`AdminSearchController` (`POST admin_search/rebuild_index`, idempotent convergence rebuild). Covering search
index + filtered-unique `(variant_id, city_id, district_id)` pair (NULL participating) + `nurse_id` index.
- **Contracts:** dev/contracts/domains/search.md + openapi snapshot refreshed (yes — `search/nurses` +
`admin_search/rebuild_index` + DTOs).
- **Mocked:** `INurseSearch` → 🟢 **SQL is real** (Elastic backend 🟡 deferred, config `Search:Backend`);
`ISearchIndexMaintainer` inline path real, outbox/feeder 🟡 deferred (see reports/mocks-registry.md).
- **Gate:** build clean (0 new code warnings) / tests green (167 pass: +9 DB-backed search + 4 API integration;
affected b3/b4/b5/b6 handler tests updated for the new dependency).
- **Handoff:** backend/handoff/after-backend-phase-7.md
- **Notes for frontend:** f6-b7 = `GET api/v1/search/nurses` (public; snake_case params
`service_category_id`/`city_id` required, optional `district_id`/`nurse_gender`/`min_price`/`max_price`/
`price_unit`; `page`/`page_size` default 1/50 max 100). Returns **only searchable** nurses;
`districtId=null` result = whole city; `price` is an IRR **digit string**; sort is rating-desc only.
`required_caregiver_gender` capture into booking is **b8**.
## backend-phase-6 — Nurse verification & credentials (mocked vendors) — 2026-07-02
- **Shipped:** the trust engine via one additive migration — new **`verif`** schema, **5 tables**:
`NurseVerifications` (`status` = the **single source of verification truth**), `VerificationStepTypes`