5839b3508f
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>
16 KiB
16 KiB
Backend status log (append-only)
One block per completed backend phase. Newest at the top. Backend lane writes here; frontend reads.
backend-phase-7 — Search & matching (nurse search index) — 2026-07-05
- Shipped: the discovery layer via one additive migration — new
searchschema, 1 tableNurseSearchIndices(the denormalizednurse_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_searchablegate. It is a read-only projection, maintained inline in each source write's own transaction byISearchIndexMaintainer(SearchIndexMaintainer) wired into the b3/b4/b5/b6 handlers (ReindexVariant/ReindexNurse/FanOutServiceArea/RemoveServiceAreaRows+Rebuild). Read side is theINurseSearchseam — real MVP implSqlNurseSearch(reads onlyis_searchable=1; category/city/district(NULL-aware)/gender/price filters + rating sort + pagination). 2 controllers: publicSearchController(GET search/nurses) + adminAdminSearchController(POST admin_search/rebuild_index, idempotent convergence rebuild). Covering search index + filtered-unique(variant_id, city_id, district_id)pair (NULL participating) +nurse_idindex.
- rating aggregates, and the single
- 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, configSearch:Backend);ISearchIndexMaintainerinline 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 paramsservice_category_id/city_idrequired, optionaldistrict_id/nurse_gender/min_price/max_price/price_unit;page/page_sizedefault 1/50 max 100). Returns only searchable nurses;districtId=nullresult = whole city;priceis an IRR digit string; sort is rating-desc only.required_caregiver_gendercapture into booking is b8.
backend-phase-6 — Nurse verification & credentials (mocked vendors) — 2026-07-02
- Shipped: the trust engine via one additive migration — new
verifschema, 5 tables:NurseVerifications(status= the single source of verification truth),VerificationStepTypes(seeded catalog — six stable codesidentity_kyc/shahkar_match/moh_competency_license/ino_membership/criminal_record/bank_account_verification),VerificationSteps(one per required step-type; snapshotsis_automated),VerificationDocuments(metadata only — bytes never in the DB),NurseCredentials(credential_numberencrypted, never serialized). 15 endpoints across 4 controllers —nurse_verification(submit/get/upload_url/documents + automatedidentity_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), publicnurses/{id}/trust_badge.nurse_profiles.is_verifiedis the only derived boolean, flipped only inside the finalize transaction (reversed transactionally on suspend/expiry). Three new mock vendor seams (IShahkarVerifier,IIdentityKycProvider,ICredentialVerifier); reuses b3IBankAccountOwnershipVerifier+ b0IObjectStorage/IFieldEncryptor. The expiry-scan logic ships asScanExpiringCredentialsCommand; the scheduled cron is deferred (adminscan_expiringis the entry point; configverification_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/isVerifiedare read-only, server-derived — never infer verification client-side. Credential numbers never cross the wire (trust badge = types only).step.isAutomateddrives the UI (/runbutton vs upload flow). Prereqs enforced with 400 (Shahkar needs KYC; bank needs KYC + a primary b3 account). Shared-SIM / vendor fails are 200 withstepStatus:"failed"+failureReason, not HTTP errors. Documents are short-lived signed URLs. Routes are action-style POST; refresh afterselect_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) — newcatalogschemaServiceCategories/ServiceOptionGroups(nullableservice_category_id= cross-category) /ServiceOptionValues/NurseServiceVariants(PriceBIGINT IRR,PriceUnit,SessionCount?,DisplayName,OptionSetHash) /NurseServiceVariantOptions(UNIQUE(variant_id, option_group_id)); seed of 5 categories (nameFa+nameEn) viaHasData. 16 CQRS slices across 3 controllers (catalogpublic browse,admin_catalogCRUD + set-active,nurse_variantscreate/update/set-active/ list/get). Duplicate-listing guard =OptionSetHash+ filteredUNIQUE(nurse_id, service_category_id, option_set_hash) WHERE deleted_at IS NULL+ 409 pre-check. Public catalog reads cached behind aCatalogCachegeneration token (invalidate on any admin write). ShipsIVariantSnapshotSerializer(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.mdrow. - Gate: build clean (0 new code warnings) / tests green (122 pass: +10 handler/serializer unit,
+11
Baya.Test.Apiintegration). 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).
priceis a string of IRR digits; the total isprice+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.displayNameauto-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) — newgeoschemaProvinces1:NCities1:NDistricts(+NurseServiceAreas) andusr.CustomerAddresses; seed (31 provinces + capital cities + Tehran's 22 مناطق viaHasData); 20 CQRS slices across 4 controllers (geopublic lookups incl./tree,admin_geoCRUD + set_active,nurse_service_areas,customer_addresses); newIGeocoderseam (deterministic mock,Seams:Geocoding); per-domain repos onIUnitOfWork; enc value converters for the address PII columns;409 Conflictadded toOperationResult/BaseController. Whole-city (district_id NULL) uniqueness via a filtered-index pair; single-primary address via filteredUNIQUE(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.Apiintegration, +12 handler unit tests). MigrationGeographyAddressesServiceAreasapplies on startup; swagger exposes all b4 paths. - Handoff: backend/handoff/after-backend-phase-4.md
- Notes for frontend:
districtId=nullmeans whole city (a real choice) everywhere. Duplicate service area → 409. Addresses come back decrypted for the owner withlatitude/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
usrtables via one migration (IdentityProfilesPatientsBankAccounts) —NurseProfiles(1:1Users; guardedis_verifiedno public setter; read-only aggregates; soft-delete),CustomerProfiles(thin payer; enc emergency contact),Patients(care recipient, tenancy-scoped;is_activearchive; encinitial_medical_notes),NurseBankAccounts(enciban+UNIQUE(iban_hash)+ filteredUNIQUE(nurse_id) WHERE is_primary=1; استعلام شبا inquiry fields); 15 CQRS slices across 4 controllers (nurse_profiles,customer_profiles,patients,nurse_bank_accounts); newIBankAccountOwnershipVerifierseam (mock = deterministic شبا match); per-domain repositories onIUnitOfWork; enc value converters for the new PII columns. Also activated FluentValidation repo-wide (AddApplicationServicesnow registers everyAbstractValidator<T>— theValidateCommandBehavior/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.Apiintegration, +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_rolebefore calling these. IBAN comes back masked (last-4).isVerified/aggregates are read-only. Patientget/updateof 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; newusr.UserSessions(refresh-token rotation + revoke-all on replayed token);usr.Usersextended (Gender,NationalIdenc NULL,ShahkarVerifiedAtauto-reset on phone change,PhoneHashUNIQUE,IsActive,DeletedAt+ soft-delete filter); phone/email/national-id encrypted at rest (EF value converter overIFieldEncryptor);usr.UserRolesgrant/revoke audit trail + revoked filter; 7 roles seeded;ISmsSenderseam (mock logs the code); 3 auth config keys;OperationResult/BaseControllerlearned 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.Apiintegration + 14 new handler unit tests). MigrationIdentitySessionsAndUserExtensionsapplied 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 theotp/requestsketch). Bodies camelCase. Fresh users:roles: []→ role router →me/select_role→ refresh tokens to pick up role claims./mephone 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, newopsschema) with 6 tables (PlatformConfigs,AuditLogs,SystemEvents,IranianHolidays,Notifications,SupportAlerts) + seed (12 config keys, 7 holidays); platform-signal facadesIPlatformConfig/IHolidayCalendar/IAnalyticsSink/IAuditLogger/INotificationService/ISupportAlertService(Persistence/Services/);AuditFieldInterceptorextended to write append-onlyaudit_logsrows forIAuditableentities; real in-appINotificationDispatcher(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, retentionIJobScheduler→ 🟡;INotificationDispatcherflipped 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_jsonis a typed deep-link payload). f15 = adminplatform_config/*,holidays/*,audit/get_audit_trail,support_alerts/*(DynamicPermission). Paginationpage/page_size(default 50, max 100).
backend-phase-0 — Foundation, cross-cutting seams & starter cleanup — 2026-06-28
- Shipped: removed the
Orderdemo (entity/feature/repo/config/gRPC) + 3 old migrations; freshInitialBaselinemigration; REST surface (PingController+System/PingCQRS);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.jsonpublished (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; ping200; rate-limit429). - Handoff: backend/handoff/after-backend-phase-0.md
- Notes for frontend:
ApiResultenvelope is fixed (camelCase body, snake_case URLs);GET /api/v1/ping/get_statusis live to wire types against;429on over-limit.