Files
baya-monorepo/dev/shared-working-context/backend/STATUS.md
T
2026-07-13 00:49:24 +03:30

401 lines
35 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>
-->
## refinement-phase-1 — Database: local-dev story, demo seed & migration hygiene — 2026-07-13
- **Shipped (no migration, no endpoint, no contract change):** Development-gated **demo-world seeder**
`Persistence/Services/Seeding/DemoWorldSeeder.cs` + `DemoWorldDefinitions.cs`, scoped-registered in
`AddPersistenceServices`, invoked from `Program.cs` via new `SeedDemoWorldAsync()` **only under
`IsDevelopment()`**. Idempotently (guarded on phone) creates **3 nurses** (2 verified: `MarkVerified()` +
`nurse_verifications` `approved` + credentials + `matched_national_id` primary bank + 23 IRR variants +
Tehran areas incl. whole-city `district_id=NULL`; 1 unverified), **2 customers** (patients + coord
addresses), and **1 cross-category required demo option group** (شیفت / Shift Type). Search rows are driven
through the real `ISearchIndexMaintainer.RebuildAsync` — never hand-inserted, so `is_searchable` stays
truthful (verified surface, unverified doesn't). Reference `HasData` seeds + the 17 migrations untouched.
- **Local DB / migration hygiene:** verified Phase 0's compose + placeholder connstrings + boot-migrate;
documented the explicit `dotnet ef database update` path + demo-reset flow in the RUNBOOK; confirmed no
pending model changes. Forward-dep FKs (Phase 6) and the boot-migrate multi-instance split (Phase 7) are
out of scope by design.
- **Contracts:** none produced; swagger snapshot **not** regenerated (no route/shape change).
- **Mocked:** none introduced (the seeder uses the real handlers/maintainer/converters).
- **Gate:** build clean (0 new warnings) / tests green (**372**: +3 `DemoWorldSeederTests` proving over the
real HTTP pipeline — verified nurse in search, unverified not, trust badge correct, idempotent). Added
`InternalsVisibleTo("Baya.Test.Api")` on Persistence for the seeder test.
- **Handoff:** backend/handoff/after-refinement-phase-1.md
- **Notes for frontend:** demo accounts to log in as (phone-OTP) — nurses `09120000001` (verified) /
`09120000002` (verified) / `09120000003` (unverified); customers `09120000010` / `09120000011`. Tehran
`city_id=101`. `GET /search/nurses?service_category_id=1&city_id=101` now returns real verified nurses.
**No `USE_*_MOCK` flag flipped — de-mocking is Phase 4.**
## refinement-phase-0 — Local end-to-end bring-up & the integration seam — 2026-07-12
- **Shipped (integration/plumbing — no business logic):** **CORS** (`Baya.WebFramework/ServiceConfiguration/
CorsServiceExtension.cs` → `AddCorsPolicies`, policy `BalinyaarWebClient` from `Cors:AllowedOrigins`, default
`http://localhost:3000`; `app.UseCors` after `UseRouting` / before `UseRateLimiter`; no `AllowCredentials`).
**Local DB story** — `server/docker-compose.yml` rewritten to SQL Server 2022 Developer on `localhost:1433`
(dev-only SA password); committed `appsettings*.json` connection strings replaced with **non-working
placeholders** (real value via `dotnet user-secrets`); added `<UserSecretsId>` to the API csproj.
**Development-only OTP helper** `GET /api/v1/dev/last_otp/{phone}` (`DevController` + `DevOtpStore` +
`DevCapturingSmsSender`, wired only in Development via `AddDevelopmentOtpCapture`; **404 outside
Development**). Wrote `dev/post-phase/refinement/RUNBOOK.md`.
- **Contracts:** none produced; swagger snapshot **not** regenerated (only new path is the dev-only helper).
- **Mocked:** no new seam; `ISmsSender` (`LoggingSmsSender`) row updated in mocks-registry (interim OTP channel
+ the Development-only capture affordance).
- **Gate:** build clean (0 new warnings) / tests green (**369**: 4 identity + 248 foundation + 117 API, incl.
3 new CORS/dev-otp integration tests + 8 dev-otp store/decorator unit tests).
- **Handoff:** backend/handoff/after-refinement-phase-0.md
- **Notes for frontend:** the client now really reaches the API cross-origin (CORS unblocked). **No mock flag
was flipped — `auth` is still the only real domain** (that's Phase 4). `client/.env.development` already
points at `https://localhost:5002`; the API speaks HTTP/2 (browsers negotiate h2-over-TLS automatically).
Read the OTP from the server console or `GET /api/v1/dev/last_otp/{phone}` (Development only).
## backend-phase-15 — Messaging (tickets), partner centers & admin backoffice — 2026-07-10
- **Shipped (FINAL backend phase):** new `messaging` schema — `Tickets` (`UNIQUE(reference_code)`, status/
category, nullable `booking_id`/`refund_id`), `TicketParticipants` (`UNIQUE(ticket_id, user_id)`, soft-remove
via `removed_at`), `TicketMessages` (`is_internal` hard boundary) — and new `partner` schema — `PartnerCenters`
(`IAuditable`, encrypted+masked `settlement_iban`, `commission_rate` separate from `platform_fee_rate`). Added
the `nurse_profiles.partner_center_id` FK in place. One migration (`MessagingAndPartnerCenters`). CQRS:
OpenTicket / AutoCreateCoordinationTicket / PostMessage / Add+RemoveParticipant / Close+ReopenTicket /
LogEmergencyTicket / GetTicketThread (user vs admin view) / ListMyTickets / ListTicketsForAdmin; CreatePartnerCenter
/ UpdatePartnerCenter / VerifyPartnerCenter / SponsorNurse / GetCenterForBooking / ListPartnerCenters /
GetPartnerCenterById / GetCenterDashboard. 5 new controllers (Tickets/AdminTickets/AdminPartnerCenters/Centers/
InternalCenters). Wired: b11 `IssueInvoice` now resolves issuer/settlement via `GetCenterForBooking`; b11
`CreateRefund` auto-opens a `refund` ticket (so `refunds.ticket_id` is always non-null); the card confirm + BNPL
settle handlers auto-create the coordination ticket. Support-alert worklist + audit viewer reused from b1 (not
rebuilt). The 4 DEFERRED tables were **not** created.
- **Contracts:** `dev/contracts/domains/messaging-notifications-admin.md` + openapi snapshot refreshed (yes).
- **Mocked:** `ILicenseVerificationService` (eNamad / MoH permit — manual-approve at MVP) → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new code warnings) / tests green (358 total: 4 identity + 240 foundation + 114 API,
incl. 4 new merchant-of-record resolver tests + 8 new ticket/partner-center API tests).
- **Handoff:** backend/handoff/after-backend-phase-15.md
- **Notes for frontend:** `is_internal` is stripped from the user thread view server-side (never trust the UI);
no direct nurse↔customer channel / no phone numbers; ticket↔booking/refund links are optional (nullable);
duplicate participant add = 409; `settlement_iban` is only ever returned masked (last 4); merchant-of-record
(invoice issuer + settlement) follows `partner_centers`, resolved by `GET /internal/bookings/{id}/center`.
## backend-phase-14 — Reviews, ratings & patient care records — 2026-07-09
- **Shipped:** new `reviews` schema, 4 tables — `Reviews` (`UNIQUE(booking_id)`, `CHECK(rating 15)`, guarded
`moderation_status`, `IAuditable`), `ReviewTagsMaster` (seeded 5-tag vocab, `UNIQUE(code)`), `ReviewTagLinks`
(`UNIQUE(review_id, review_tag_master_id)`), `PatientCareRecords` (encrypted, patient-scoped, `(patient_id,
recorded_at)` index). One migration (`ReviewsAndPatientCareRecords`). CQRS: SubmitReview / ModerateReview /
AttachReviewTags / ListReviewsForNurse / GetReviewModerationQueue / GetTagAggregates / WritePatientCareRecord /
GetPatientHistory + the `RecomputeNurseRating` from-source helper. 8 endpoints across 5 controllers. Added
`NurseProfile.SetReviewAggregates` (guarded aggregate write) + `ICustomerProfileRepository.GetUserIdByProfileIdAsync`.
- **Contracts:** `dev/contracts/domains/reviews-records.md` + openapi snapshot refreshed (yes).
- **Mocked:** `IReviewModerationService` (AI pre-screen — keyword/pass-through) → 🟡 (see reports/mocks-registry.md).
- **Gate:** build clean (0 new warnings) / tests green (346 total: 4 identity + 236 foundation + 106 API,
incl. 13 new review/care-record handler tests + 4 new API tests).
- **Handoff:** backend/handoff/after-backend-phase-14.md
- **Notes for frontend:** publish gate — only `published` reviews are ever public/counted; the nurse aggregate
recomputes from source on every transition. Care records are patient-scoped + encrypted + strict access
(owner/assigned-nurse/admin). Money-free domain. Support alerts stay internal (only `lowRatingAlertId` on the
admin queue).
## backend-phase-13 — Weekly nurse payouts (mocked PAYA/SATNA) — 2026-07-09
- **Shipped:** new `payouts` schema, 3 tables — `NursePayoutBatches` (holiday-shifted period/processing dates),
`NursePayouts` (net-split CHECK, encrypted `iban_snapshot`, forward-only `PayoutStatus`), `NursePayoutBookingLinks`
(**unconditional `UNIQUE(booking_id)`** = one-payout-per-booking-ever). One migration (`NursePayoutEngine`).
`Features/Payouts/*` (compute-eligible / generate-batch / process / retry / mark-failed + admin batch-detail/list +
nurse history; shared `PayoutSettlement` step). `IPayoutRepository`. Controllers `AdminPayouts` (admin, rate-limited)
+ `NursePayouts` (nurse, tenancy-scoped). New seam **`IBankTransferProvider`** (mock PAYA/SATNA). Swapped
`INursePayoutStatus` to the authoritative link-based `NursePayoutLinkStatusService` (deleted the interim one). Added
2 config keys (`payout_satna_threshold_irr`, `require_bnpl_settlement_for_payout`).
- **Contracts:** `dev/contracts/domains/payouts.md` + openapi snapshot refreshed (yes — 7 payout paths).
- **Mocked:** `IBankTransferProvider` → 🟡; `INursePayoutStatus` → 🟢 (real link lookup). Reuse `IHolidayCalendar`,
`IFieldEncryptor`, `IDistributedLock`, `ICacheService`. See reports/mocks-registry.md.
- **Gate:** build clean (0 new code warnings) / tests green (329: 223 foundation + 102 api + 4 identity; +9 payout
unit + 6 payout api). Migration builds; swagger serves all 7 payout paths.
- **Handoff:** backend/handoff/after-backend-phase-13.md
- **Notes for frontend:** f12-b13 = nurse `nurse_payouts/history` (own payouts, masked IBAN, digit-string money) +
admin payout console (`admin_payouts/eligible|batches|batches/{id}|batches/{id}/process|{payoutId}/retry|mark_failed`).
Query params camelCase (`page`/`pageSize`/`status`/`periodStart`/`periodEnd`). Money is a digit string.
## backend-phase-12 — BNPL: provider-financed installments (mocked) — 2026-07-09
- **Shipped:** `payments.BnplTransactions` (1:1 with `payment_transaction`, `UNIQUE(payment_transaction_id)`,
settle-split CHECK, forward-only `BnplStatus` machine); `Features/Bnpl/*` (eligibility/initiate/verify/settle/
revert/callback/status); `CheckoutBnplController` (customer) + `WebhooksBnplController` (anon, signed) +
`AdminBnplController` (admin), all rate-limited; `LedgerPosting.BnplSettle` (net-of-fee group w/
`bnpl_fee_expense`); extracted shared `Features/Bookings/BookingConversion` (used by b10 card + b12 settle).
- **Contracts:** dev/contracts/domains/bnpl.md + openapi snapshot refreshed (yes).
- **Mocked:** `IBnplProvider` (per `provider_code` via `IBnplProviderResolver`) + `ICurrencyNormalizer` → 🟡
(configurable mock commission %, non-instant `settled_at`). See reports/mocks-registry.md.
- **Gate:** build clean (0 new code warnings) / tests green (314 pass: 4 identity + 214 foundation + 96 api;
+14 new). Migration `BnplTransactions` created (not applied to a live DB this session — SQLite
`EnsureCreated` builds it for tests).
- **Handoff:** backend/handoff/after-backend-phase-12.md
- **Notes for frontend:** f11-b12 = the "pay with installments" checkout (`checkout_bnpl/eligibility` →
`initiate` → provider redirect; declined → fall back to card), the customer order view (`checkout_bnpl/{id}`),
and the admin BNPL revert path with the ~710-day ETA. Money is IRR digit-strings; `settledAt` is nullable
(not instant). A BNPL revert opens a `refund_channel='bnpl_revert'` refund (read via `refunds/{id}/status`).
## backend-phase-11 — Refunds, invoices & nurse clawbacks — 2026-07-09
- **Shipped:** the reversal leg via one migration in the **`payments`** schema — **3 tables** `Refunds`
(fee-leg decomposition + `refund_channel` + `amount = fee_leg + payout_leg` CHECK + **nullable `ticket_id`, no FK**
until b15) / `NurseClawbacks` (nullable `original_payout_id`/`recovered_in_payout_id` until b13) / `Invoices`
(**UNIQUE `invoice_number`** sequential + UNIQUE `booking_id`), plus the `InvoiceNumberSequences` counter row.
Features: `CreateRefund` (whole money-path under `lock(booking:{id}:refund)`; decompose → Σ≤captured → channel →
balanced ledger reversal via b10's helper; pre-payout `nurse_payable` vs post-payout `nurse_clawback_receivable`
fork + `pending` clawback + support alert), `WriteOffClawback`, `ListRefunds`, `GetRefundStatus`, `IssueInvoice`
(VAT on commission only from config, sequential number, mocked مودیان), `GetInvoice`. Controllers: admin
`AdminRefunds`/`AdminClawbacks`/`AdminInvoices` (rate-limited) + customer `Refunds`/`Invoices`.
- **Contracts:** dev/contracts/domains/refunds-invoices.md + openapi snapshot refreshed — **yes**.
- **Mocked:** `IMoadianClient` (new), `IBnplProvider` (thin pre-b12 stub), `INursePayoutStatus` (interim derivation;
b13 owns real); reused `IPaymentProvider`/`IWebhookVerifier`/`IDistributedLock`/`INotificationDispatcher`. See
reports/mocks-registry.md.
- **Gate:** build clean (0 new warnings) / tests green (300 pass: 205 Foundation + 91 Api + 4 Identity). Migration
applied to the dev DB; swagger snapshot republished.
- **Handoff:** backend/handoff/after-backend-phase-11.md
- **Notes for frontend:** refunds are **admin-only** (no self-service); the only customer surfaces are
`GET refunds/{id}/status` (with the BNPL `expected_customer_refund_eta` date + masked reference) and
`GET invoices/{booking_id}`. Money is IRR digit-strings. Card refund = immediate `succeeded`; BNPL = `processing`
+ ETA.
## backend-phase-10 — Payments core: ledger, transactions, webhooks & card capture — 2026-07-06
- **Shipped:** the money core via one migration in a new **`payments`** schema — **4 tables** `PaymentGateways`
(encrypted `config_json`) / `PaymentTransactions` (the **two filtered uniques** — `gateway_reference_code` WHERE
NOT NULL, `booking_id` WHERE status='succeeded') / `PaymentWebhookEvents` (**UNIQUE(provider_code,
external_event_id)**) / **append-only** `LedgerEntries`. Features `InitiatePayment`, `HandlePaymentWebhook`,
`ConfirmPaymentAndPostLedger` (internal), `GetNursePayableBalance`; controllers
`POST bookings/{id}/payments`, public `POST webhooks/payments/{provider}`, `GET nurses/{id}/payable_balance`.
Extracted **`BookingFactory`** so b10's real capture reuses b9's conversion (b9 unchanged).
- **Contracts:** `dev/contracts/domains/payments.md` written; **openapi snapshot refreshed**
(`dev/contracts/openapi/swagger.v1.json` — the three b10 paths + DTOs present).
- **Mocked:** `IPaymentProvider`, `ISettlementSplitProvider`, `IWebhookVerifier`, `IDistributedLock` → 🟡
(see reports/mocks-registry.md). `IPaymentCaptureSimulator` (b9) retained for b9's Convert path/tests.
- **Gate:** build clean (0 new warnings) / tests green (Foundation 198, Identity 4, Api 83; +12 Foundation +4 Api new).
- **Handoff:** backend/handoff/after-backend-phase-10.md
- **Notes for frontend:** money is an **IRR digit-string**; a booking exists only **on capture** (pay against the
accepted **request** id; the booking appears `confirmed` after the webhook); **never expose internal
`account_type`s** (checkout = gross + commission/VAT); payment idempotent end-to-end (`Idempotency-Key` header).
## backend-phase-9 — Bookings, sessions, care instructions & EVV — 2026-07-06
- **Shipped:** the post-payment engine via one additive migration in the **`booking`** schema — **5 tables**
`Bookings` / `BookingSessions` / `BookingCareInstructions` / `VisitVerifications` / `CancellationPolicies`
(seeded 4 tiers) + 2 config rows (`no_show_threshold_minutes`, `no_show_scan_cadence_hours`). `Convert`
(mock-capture → booking 1:1, three-amount split, snapshots, N sessions), care-instructions submit + **gated**
read, EVV `check_in`/`check_out` (advisory address match, dispute window on completion), `transition`,
`cancel` booking/session (policy snapshot + un-started refund), `detect_no_shows`, cancellation-policy CRUD.
Controllers: `Bookings` / `BookingSessions` / `AdminEvv` / `AdminCancellationPolicies`.
- **Contracts:** dev/contracts/domains/bookings-evv.md + openapi snapshot refreshed (yes).
- **Mocked:** **`IPaymentCaptureSimulator`** introduced (🟡) — the temporary conversion trigger; reuses
`IGeocoder`/`IFieldEncryptor`/`INotificationDispatcher`/`ISupportAlertService`. Real capture trigger = b10.
- **Gate:** build clean (0 new code warnings) / tests green (269: 186 foundation + 4 identity + 79 API).
- **Handoff:** backend/handoff/after-backend-phase-9.md
- **Notes for frontend (f8-b9):** money is an **IRR digit-string** (three amounts reconcile). The nurse view
omits `addressSnapshotJson`; care-instruction clinical fields are **assigned-nurse/admin only, post-confirmation**
(`GET bookings/care_instructions/{id}`); raw EVV GPS is gated to owning nurse + admin. Routes are action-style
(`bookings/get/{id}`, `booking_sessions/check_in/{id}`, `booking_sessions/today`, `admin_evv/list`). Payout
eligibility comes from `disputeWindowEndsAt`/`payoutEligibleAt`, never `completed` alone.
## backend-phase-8 — Booking requests (pre-payment intent) — 2026-07-06
- **Shipped:** the money-free request lifecycle via one additive migration — new **`booking`** schema,
**1 table** `BookingRequests`. The customer-side (`CreateBookingRequest` — tenancy invariant
patient+address∈customer & variant∈nurse, bookability + **same-gender** match, response deadline frozen from
`nurse_response_deadline_hours`; `CancelBookingRequest`), the nurse-side (`AcceptBookingRequest` — freezes
`payment_deadline_at` from `booking_payment_deadline_minutes`=30, self-guards the response deadline;
`RejectBookingRequest` — reason required), the reads (`ListBookingRequests` role-scoped inbox +
`GetBookingRequest` party/admin, **nurse view masks the full address, exposes only `customer_notes`**), and
the **forward-only status guard** (`BookingRequestTransitions`) + the idempotent, paginated
`ExpireBookingRequests` sweep behind a recurring `BackgroundService` (`BookingRequestExpiryHostedService`,
reuses the b1 `IJobScheduler` seam) + an admin manual trigger. **2 controllers:** `BookingRequestsController`
(customer/nurse) + `AdminBookingRequestsController` (`POST admin_booking_requests/expire`). Inbox/expiry
covering indexes `(nurse_id,status)`/`(customer_id,status)`/`(status,response_deadline)`/`(status,payment_deadline)`.
**No money column, no `bookings` row, no snapshot, no price** anywhere.
- **Contracts:** dev/contracts/domains/booking-requests.md + openapi snapshot refreshed (yes — the 7 booking
paths + `BookingRequestDto`/`BookingRequestListItemDto`/`ExpireBookingRequestsResult`).
- **Mocked:** **nothing new** — reuses `IPlatformConfig`/`INotificationDispatcher`/`IJobScheduler`/
`IDateTimeProvider`/`ICurrentUser`. **No new `mocks-registry.md` row** (the `IJobScheduler` row was updated to
note the second hosted job).
- **Gate:** build clean (0 new code warnings) / tests green (215 pass: +20 booking = 14 handler-unit + the
transition machine + 4 DB-backed SQLite + 5 API integration). Migration applied to the dev DB; API boots with
all booking paths in Swagger; the expiry sweep runs on startup (verified against SQL Server).
- **Handoff:** backend/handoff/after-backend-phase-8.md
- **Notes for frontend:** f7-b8 = the request form (C4 → `booking_requests/create`), the status tracker (C5 →
`booking_requests/get/{id}` + customer inbox with the response + 30-min payment countdowns + `cancel`), and
the nurse inbox (`booking_requests/list?role=nurse` + `accept`/`reject`). Gender required (`male`/`female`/
`any`); deadlines are server-frozen UTC timestamps; the nurse sees only `customer_notes` + a masked address.
## 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.