Files
baya-monorepo/dev/shared-working-context/backend/STATUS.md
T
hamid 5839b3508f 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>
2026-07-05 17:24:26 +03:30

189 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backend status log (append-only)
One block per completed backend phase. Newest at the top. Backend lane writes here; frontend reads.
<!-- TEMPLATE — copy for each phase
## backend-phase-N — <Title> — <YYYY-MM-DD>
- **Shipped:** <entities/endpoints/seams in one or two lines>
- **Contracts:** dev/contracts/domains/<domain>.md + openapi snapshot refreshed (yes/no)
- **Mocked:** <which seams> (see reports/mocks-registry.md)
- **Gate:** build clean / tests green
- **Handoff:** backend/handoff/after-backend-phase-N.md
- **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`
(seeded catalog — six stable codes `identity_kyc`/`shahkar_match`/`moh_competency_license`/`ino_membership`/
`criminal_record`/`bank_account_verification`), `VerificationSteps` (one per required step-type; snapshots
`is_automated`), `VerificationDocuments` (**metadata only** — bytes never in the DB), `NurseCredentials`
(`credential_number` **encrypted, never serialized**). **15 endpoints across 4 controllers**
`nurse_verification` (submit/get/upload_url/documents + automated `identity_kyc`/`shahkar_match`/
`bank_account_verification` `/run`), `admin_verification_step_types` (list/upsert/deactivate, dup code →
409), `admin_verifications` (queue/detail/decide/suspend/scan_expiring), public `nurses/{id}/trust_badge`.
`nurse_profiles.is_verified` is the **only derived boolean**, flipped **only inside the finalize
transaction** (reversed transactionally on suspend/expiry). Three **new mock vendor seams**
(`IShahkarVerifier`, `IIdentityKycProvider`, `ICredentialVerifier`); reuses b3
`IBankAccountOwnershipVerifier` + b0 `IObjectStorage`/`IFieldEncryptor`. The expiry-scan logic ships as
`ScanExpiringCredentialsCommand`; the **scheduled cron is deferred** (admin `scan_expiring` is the entry
point; config `verification_expiry_scan_cadence_hours`, default 24).
- **Contracts:** dev/contracts/domains/verification.md + openapi snapshot refreshed (yes — all 15 b6 paths).
- **Mocked:** `IShahkarVerifier`, `IIdentityKycProvider`, `ICredentialVerifier` → 🟡 (deterministic mocks;
see reports/mocks-registry.md). All vendor/money calls are mocked.
- **Gate:** build clean (0 new code warnings) / tests green (**153 pass**). Swagger exposes all 15 b6 paths.
- **Handoff:** backend/handoff/after-backend-phase-6.md
- **Notes for frontend:** **`isBookable`/`isVerified` are read-only, server-derived** — never infer
verification client-side. **Credential numbers never cross the wire** (trust badge = types only).
`step.isAutomated` drives the UI (`/run` button vs upload flow). Prereqs enforced with **400** (Shahkar
needs KYC; bank needs KYC + a primary b3 account). Shared-SIM / vendor fails are **200 with
`stepStatus:"failed"` + `failureReason`**, not HTTP errors. Documents are short-lived signed URLs. Routes
are action-style POST; refresh after `select_role`. Public trust badge (f6) is `[AllowAnonymous]`.
## backend-phase-5 — Service catalog & nurse pricing variants — 2026-07-02
- **Shipped:** five tables via one additive migration (`ServiceCatalogAndNurseVariants`) — new **`catalog`**
schema `ServiceCategories` / `ServiceOptionGroups` (nullable `service_category_id` = cross-category) /
`ServiceOptionValues` / `NurseServiceVariants` (`Price` **BIGINT IRR**, `PriceUnit`, `SessionCount?`,
`DisplayName`, `OptionSetHash`) / `NurseServiceVariantOptions` (`UNIQUE(variant_id, option_group_id)`);
seed of **5 categories** (`nameFa`+`nameEn`) via `HasData`. 16 CQRS slices across 3 controllers
(`catalog` public browse, `admin_catalog` CRUD + set-active, `nurse_variants` create/update/set-active/
list/get). Duplicate-listing guard = `OptionSetHash` + filtered `UNIQUE(nurse_id, service_category_id,
option_set_hash) WHERE deleted_at IS NULL` + 409 pre-check. Public catalog reads cached behind a
`CatalogCache` generation token (invalidate on any admin write). Ships **`IVariantSnapshotSerializer`**
(pure, for b8). **No new seam.**
- **Contracts:** dev/contracts/domains/catalog.md + openapi snapshot refreshed (yes — 14 new
catalog/admin_catalog/nurse_variants paths; 71 total).
- **Mocked:** none — this phase mocks nothing and adds **no** `reports/mocks-registry.md` row.
- **Gate:** build clean (0 new code warnings) / tests green (122 pass: +10 handler/serializer unit,
+11 `Baya.Test.Api` integration). Migration verified to apply on a real SQL Server; swagger exposes all
b5 paths. Adversarial 4-dimension review: 0 confirmed findings.
- **Handoff:** backend/handoff/after-backend-phase-5.md
- **Notes for frontend:** the **variant is the bookable unit** (not the nurse). `price` is a **string of IRR
digits**; the total is `price` + `priceUnit` + `sessionCount`, never price alone. A **NULL-category option
group is cross-category** (render it under every category; required ones must be answered). Duplicate
identical listing → **409**; missing required dimension → **400**. `displayName` auto-generates (editable).
Deactivate, never delete. Routes are action-style POST (`admin_catalog/create_category`,
`nurse_variants/create`, …); groups/values are admin-authored (only categories are seeded).
## backend-phase-4 — Geography, addresses & nurse service areas — 2026-07-02
- **Shipped:** five tables via one migration (`GeographyAddressesServiceAreas`) — new **`geo`** schema
`Provinces` 1:N `Cities` 1:N `Districts` (+ `NurseServiceAreas`) and `usr.CustomerAddresses`; seed
(31 provinces + capital cities + Tehran's 22 مناطق via `HasData`); 20 CQRS slices across 4 controllers
(`geo` public lookups incl. `/tree`, `admin_geo` CRUD + set_active, `nurse_service_areas`,
`customer_addresses`); new **`IGeocoder`** seam (deterministic mock, `Seams:Geocoding`); per-domain
repos on `IUnitOfWork`; enc value converters for the address PII columns; **`409 Conflict`** added to
`OperationResult`/`BaseController`. Whole-city (`district_id NULL`) uniqueness via a **filtered-index
pair**; single-primary address via filtered `UNIQUE(customer_id) WHERE is_primary=1`; geo reads cached
behind a generation-token scheme with invalidate-on-admin-write.
- **Contracts:** dev/contracts/domains/geography-addresses.md + openapi snapshot refreshed (yes — 20 new
geo/service-area/address paths).
- **Mocked:** `IGeocoder` → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new code warnings) / tests green (103 pass: +16 `Baya.Test.Api` integration,
+12 handler unit tests). Migration `GeographyAddressesServiceAreas` applies on startup; swagger exposes
all b4 paths.
- **Handoff:** backend/handoff/after-backend-phase-4.md
- **Notes for frontend:** `districtId=null` means **whole city** (a real choice) everywhere. Duplicate
service area → **409**. Addresses come back **decrypted for the owner** with `latitude`/`longitude`
(nullable when ungeocoded — geocoding is mocked). Routes are action-style (`admin_geo/create_city`,
`nurse_service_areas/add`, `customer_addresses/create`, …). Admin geo needs an admin token.
## backend-phase-3 — Identity: profiles, patients & nurse bank accounts — 2026-07-02
- **Shipped:** four `usr` tables via one migration (`IdentityProfilesPatientsBankAccounts`) —
`NurseProfiles` (1:1 `Users`; guarded `is_verified` **no public setter**; read-only aggregates;
soft-delete), `CustomerProfiles` (thin payer; enc emergency contact), `Patients` (care recipient,
tenancy-scoped; `is_active` archive; enc `initial_medical_notes`), `NurseBankAccounts` (enc `iban` +
`UNIQUE(iban_hash)` + filtered `UNIQUE(nurse_id) WHERE is_primary=1`; استعلام شبا inquiry fields);
15 CQRS slices across 4 controllers (`nurse_profiles`, `customer_profiles`, `patients`,
`nurse_bank_accounts`); new **`IBankAccountOwnershipVerifier`** seam (mock = deterministic شبا match);
per-domain repositories on `IUnitOfWork`; enc value converters for the new PII columns. Also
**activated FluentValidation** repo-wide (`AddApplicationServices` now registers every
`AbstractValidator<T>` — the `ValidateCommandBehavior`/model-state filter were previously starved).
- **Contracts:** dev/contracts/domains/identity-profiles.md + openapi snapshot refreshed (yes — new
nurse/customer/patient/bank paths).
- **Mocked:** `IBankAccountOwnershipVerifier` → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new code warnings) / tests green (75 pass: +13 `Baya.Test.Api` integration,
+15 handler unit tests). Migration applied to the dev DB on startup; swagger exposes all b3 paths.
- **Handoff:** backend/handoff/after-backend-phase-3.md
- **Notes for frontend:** role scoping needs the **role claim in the token** — refresh after
`select_role` before calling these. IBAN comes back **masked** (last-4). `isVerified`/aggregates are
read-only. Patient `get/update` of another customer's id → **404**. Addresses/service-areas are
**deferred to b4**.
## backend-phase-2 — Identity: phone-OTP auth, sessions & roles (REST) — 2026-07-02
- **Shipped:** the six-endpoint REST auth surface (`auth/request_otp`, `auth/verify_otp`,
`auth/refresh`, `auth/logout`, `me`, `me/select_role`) wrapping the existing JWE/TOTP/RBAC engine;
new `usr.UserSessions` (refresh-token rotation + revoke-all on replayed token); `usr.Users` extended
(`Gender`, `NationalId` enc NULL, `ShahkarVerifiedAt` auto-reset on phone change, `PhoneHash`
UNIQUE, `IsActive`, `DeletedAt` + soft-delete filter); phone/email/national-id **encrypted at rest**
(EF value converter over `IFieldEncryptor`); `usr.UserRoles` grant/revoke audit trail + revoked
filter; 7 roles seeded; `ISmsSender` seam (mock logs the code); 3 auth config keys;
`OperationResult`/`BaseController` learned 401/403.
- **Contracts:** dev/contracts/domains/identity-auth.md + openapi snapshot refreshed (yes — 22 paths).
- **Mocked:** `ISmsSender` → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new code warnings) / tests green (47 pass: 10 new `Baya.Test.Api`
integration + 14 new handler unit tests). Migration `IdentitySessionsAndUserExtensions` applied to
the dev DB; full §7 flow verified live (OTP in log, tokens, 401/403/429, rotation, replay-revoke,
logout stamp-kill).
- **Handoff:** backend/handoff/after-backend-phase-2.md
- **Notes for frontend:** exact paths are `request_otp`/`verify_otp`/`select_role` (snake_case
transformer — not the `otp/request` sketch). Bodies camelCase. Fresh users: `roles: []` → role
router → `me/select_role`**refresh tokens** to pick up role claims. `/me` phone is masked.
SMS is mocked — read the OTP from the server log.
## backend-phase-1 — Config, reference & platform signals — 2026-07-02
- **Shipped:** first marketplace migration baseline (`InitialMarketplaceBaseline`, new **`ops`** schema)
with 6 tables (`PlatformConfigs`, `AuditLogs`, `SystemEvents`, `IranianHolidays`, `Notifications`,
`SupportAlerts`) + seed (12 config keys, 7 holidays); platform-signal facades `IPlatformConfig` /
`IHolidayCalendar` / `IAnalyticsSink` / `IAuditLogger` / `INotificationService` / `ISupportAlertService`
(`Persistence/Services/`); `AuditFieldInterceptor` extended to write append-only `audit_logs` rows for
`IAuditable` entities; real in-app `INotificationDispatcher` (b0 stub removed); notification-retention
hosted service; 5 controllers (admin config/holidays/audit/support-alerts + current-user notifications).
- **Contracts:** `dev/contracts/domains/config-reference.md` + openapi snapshot refreshed (yes — 16 paths).
- **Mocked:** `IHolidayCalendar`, `IAnalyticsSink`, retention `IJobScheduler` → 🟡; `INotificationDispatcher`
flipped to in-app-real 🟡 (SMS/push deferred). See reports/mocks-registry.md.
- **Gate:** build clean (0 new code warnings) / tests green (22 pass: 4 identity + 18 foundation). Migration
applied to the dev DB; API boots with all 16 paths in Swagger; retention job runs on startup.
- **Handoff:** backend/handoff/after-backend-phase-1.md
- **Notes for frontend:** f14 = `notifications/*` (envelope unchanged; unread-first lists; `data_json` is a
typed deep-link payload). f15 = admin `platform_config/*`, `holidays/*`, `audit/get_audit_trail`,
`support_alerts/*` (DynamicPermission). Pagination `page`/`page_size` (default 50, max 100).
## backend-phase-0 — Foundation, cross-cutting seams & starter cleanup — 2026-06-28
- **Shipped:** removed the `Order` demo (entity/feature/repo/config/gRPC) + 3 old migrations; fresh
`InitialBaseline` migration; REST surface (`PingController` + `System/Ping` CQRS); `ICurrentUser` +
`AuditFieldInterceptor`; five cross-cutting seams (`IDateTimeProvider`, `IFieldEncryptor`,
`ICacheService`, `IObjectStorage`, `INotificationDispatcher`) with mocks; `LoggingBehavior` +
rate limiter (per-IP global + `otp`/`auth`/`sensitive`).
- **Contracts:** `dev/contracts/openapi/swagger.v1.json` published (envelope + ping schemas).
- **Mocked:** the 5 seams above → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new warnings) / tests green (10 pass). Live API verified vs `192.168.100.14`
(migration applied + seeded; ping `200`; rate-limit `429`).
- **Handoff:** backend/handoff/after-backend-phase-0.md
- **Notes for frontend:** `ApiResult` envelope is fixed (camelCase body, snake_case URLs);
`GET /api/v1/ping/get_status` is live to wire types against; `429` on over-limit.