diff --git a/dev/post-phase/refinement/README.md b/dev/post-phase/refinement/README.md new file mode 100644 index 0000000..26a5651 --- /dev/null +++ b/dev/post-phase/refinement/README.md @@ -0,0 +1,144 @@ +# Refinement phases — making Balinyaar work as one integrated app + +**Created:** 2026-07-10 · **Scope:** the whole repo (client + server) after the 16+16 phase chain completed · +**Method:** a read-only audit of the live code (three parallel exploration passes over the frontend wiring, the +backend runnability/infra, and the frontend↔backend contract), cross-checked against `dev/` docs. + +This directory is a **runnable chain of 10 refinement phases** that takes the repo from *"both projects build +and each runs on its own, but they aren't actually integrated"* to *"one working app where the logic takes +effect."* Run them **in order, one at a time**, pointing a fresh agent at one phase file +(*"Execute `dev/post-phase/refinement/refinement-phase-0-bring-up.md` end to end"*). + +> **Why this exists alongside [`../server/`](../server/README.md).** The existing +> [server post-phase audit](../server/post-phase-backend-plan.md) is excellent — but it audits the **backend in +> isolation**. It never covers the thing you actually hit when you ran the app: the two projects were built in +> **parallel, decoupled lanes and never run against each other**. This chain adds the missing **integration + +> frontend track** (phases 0–4) and folds the server audit's 8 buckets into the back half (phases 5–9) in the +> right order. + +--- + +## Two things that are NOT wrong (correcting the brief) + +You suspected these; the audit found they're actually fine — which is good news: + +1. **"Database migrations are behind" — they're not.** There are **17 EF migrations** tracking every backend + phase b0→b15; the model snapshot is in sync and the working tree is clean. `Program.cs` applies them on every + boot. If it *felt* like migrations were behind, it's because your **target database was never migrated** (you + were running the frontend on mocks, so nothing ever touched a real DB) — not because migration files are + missing. The real DB gap is **no local-dev database story + no demo seed data** (Phases 0–1), not drift. +2. **"Missing services like Redis / Elasticsearch" — not needed for the MVP.** The **only external service the + backend requires today is SQL Server.** All 18 vendor/infra dependencies (cache, lock, jobs, storage, SMS, + payments, KYC, geocoding, search, moderation) are **in-process mocks behind DI seams — by design.** Redis + becomes necessary only when you run a **second API instance** (Phase 7); Elasticsearch is **never MVP** (the + SQL search is real and correct). You didn't miss configuring them. + +--- + +## What's actually wrong (the problem inventory) + +Every item below is verified in the live code (file/line evidence is in the phase files). + +### A. The frontend barely talks to the backend +- **21 of 22 client service domains default to an in-browser mock** (`USE_*_MOCK = true`, hard-coded literals in + each `services/{domain}/constants.ts`). **Only `auth` is real by default.** So a running app shows fake + in-memory data everywhere except login. → **Phase 4** (flip them, in dependency order). +- **The backend has no CORS at all** (zero `AddCors`/`UseCors` in `server/`). Even the calls the frontend *does* + make would be blocked by the browser once the two run on different origins. **This is the #1 hard blocker.** + → **Phase 0**. +- The client is correct and ready (`clientFetch` reads `NEXT_PUBLIC_API_URL`, unwraps the `ApiResult` envelope, + does silent 401 refresh); each domain's real `clientApi` already maps the live routes 1:1. The swap is a + **one-line flag flip per domain** — *once the backend serves what that domain needs* (see C). + +### B. "No auth / only the customer side shows" +- Auth **is** real and works — but a fresh phone login holds only the `customer` role, so `resolveRoleDestination` + sends everyone to the customer app (which lives at the root `/`). Nurse/admin shells are never auto-reached. +- A hard-coded `DEFAULT_ROLE = customer` fallback means **un-hydrated or failed `/me` role state silently renders + the customer shell** — a nurse can be shown the customer app. There are no client-side role guards. +- OTP delivery is a **log statement** (`LoggingSmsSender`), so "logging in" requires reading the code from the + server console — fine for dev, but there's no real SMS yet. → **Phase 2** (role reachability + robust + hydration + guards) and **Phase 0/8** (the dev OTP bridge, then real SMS). + +### C. Frontend ↔ backend contract mismatches (the real "mismatch" you sensed) +- The frontend filed **37 contract requests (REQ-001…037) — all still `Status: open`.** They're why 21 domains + are mock-primary: a DTO missing a field, or a customer read that only exists as an admin route. Almost all are + **additive** (nullable fields, new read endpoints). → **Phase 3**. +- Three concrete shape mismatches to reconcile before flipping the matching mock: partner-center **kebab-case** + routes that violate the snake_case convention; the BNPL **`balinyaar`** provider not in the wire enum; the + **client-invented cancellation-policy codes**. → **Phase 3**, verified in **Phase 4**. + +### D. It can't start cleanly / shows nothing +- The backend only boots against a **committed remote SQL Server** (leaked `sa` credentials); no local default, + no SQLite fallback for `dotnet run`. → **Phase 0/1** (local DB) and **Phase 5** (rotate the leak). +- A fresh DB seeds only lookup tables + one admin + one gateway — **no nurses, variants, or search rows** — so + search/discovery/booking are empty even on the real path. → **Phase 1** (demo seed). + +### E. Production-readiness gaps (from the server audit — real, but after "make it work") +- Committed secrets & dev-grade defaults (Phase 5); one genuine money hole — the **unreachable BNPL/manual refund + clearing** (Phase 6); **no unattended operation** — nurses aren't paid unless an admin clicks (Phase 7); every + vendor rail is mocked — **SMS is launch-critical** (Phase 8); metrics-only observability + doc drift (Phase 9). + +--- + +## The 10 refinement phases + +Run top to bottom. Phases 0–4 make the app **work as an integrated whole**; phases 5–9 make it +**production-ready** (and largely re-sequence the [server audit](../server/post-phase-backend-plan.md)). + +| # | Phase | Track | Fixes (from above) | Depends on | +| --- | --- | --- | --- | --- | +| **0** | [Local end-to-end bring-up & the integration seam](refinement-phase-0-bring-up.md) | both | CORS · local DB · dev OTP · one real round-trip | — | +| **1** | [Database: local-dev story, demo seed & migration hygiene](refinement-phase-1-database-and-seed.md) | backend | empty marketplace · local DB · (migration myth) | 0 | +| **2** | [Auth & role-aware navigation](refinement-phase-2-auth-and-role-nav.md) | frontend (+bit of backend) | "only customer side shows" | 0, 1 | +| **3** | [Backend contract batch (close the 37 REQ gaps)](refinement-phase-3-contract-batch.md) | backend | the mismatch — REQ-001…037 | 0 | +| **4** | [Frontend de-mock (flip every domain to real)](refinement-phase-4-frontend-de-mock.md) | frontend | "no API calls to backend" | 1, 2, 3 | +| **5** | [Security & config hygiene](refinement-phase-5-security-hygiene.md) | backend | leaked `sa` creds, keys, seeds | — (5.1 now) | +| **6** | [Money-path correctness completion](refinement-phase-6-money-correctness.md) | backend | unreachable refund clearing + FKs | before 8 | +| **7** | [Unattended ops: scheduler, locking & multi-instance](refinement-phase-7-unattended-ops.md) | backend | nurses not paid unattended · Redis gate | 6 | +| **8** | [External rails go real (SMS → trust → money)](refinement-phase-8-external-rails.md) | backend | mocks → real vendors (SMS launch-critical) | 6, 7 | +| **9** | [Observability, ops hardening & scale-later](refinement-phase-9-observability-and-scale.md) | backend | tracing/health/logs · doc honesty · ES/analytics deferred | — | + +### Dependency & sequencing + +``` +Make it work (integration): + 0 bring-up ──► 1 database/seed ──► 2 auth & role nav ─┐ + └────────► 3 contract batch ────────────────────┴──► 4 frontend de-mock + (app now integrated) +Make it production-ready (server audit, re-sequenced): + 5 security (5.1 rotate creds = TODAY) + 6 money correctness ──► 8 money rails + 7 scheduler/Redis ─────► (weekly payout trigger, Moadian poll for 8) + 8 external rails (5.1 SMS = launch-critical, do first in the phase) + 9 observability & scale-later (start anytime, finish before launch) +``` + +**Two items that jump their slot:** **Phase 5 §5.1** (rotate the committed `sa` credentials — the leak is live +*today*) and **Phase 8 §5.1** (real SMS — no real user can log in without it, sequence it first inside Phase 8). + +**Minimum path to "a working demo on real data":** Phases **0 → 1 → 2 → 3 → 4**. After those, the whole +customer/nurse/admin funnel runs against the real backend with seeded data (payments still use the dev +conversion path until Phase 8 swaps a real PSP). + +--- + +## How the phase files are written + +Each file follows the repo's [phase template](../../phases/_shared/phase-template.md): a one-paragraph mission, +context (what already exists — don't rebuild), required reading, enumerated scope, the seams/mocks it touches, +the invariants it must not break, a Definition of Done, concrete how-to-test steps, and a close-out (docs + +memory). Phases 0–4 are written in full; phases 5–9 are concise and **point into the already-detailed +[server audit](../server/post-phase-backend-plan.md)** (which has file/line evidence for every item) rather than +restating it. + +## Related documents + +- [../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) — the backend-only audit's 8 + buckets (folded into Phases 5–9), with file/line evidence. +- [../server/frontend-backend-gaps.md](../server/frontend-backend-gaps.md) — REQ-001…015 reconciled against + shipped code (Phase 3 extends this through REQ-037). +- [../server/runtime-services.md](../server/runtime-services.md) — the 17-service deployment topology. +- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) + — the canonical REQ-001…037 tracker (Phase 3's spec). +- [../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md) + — every seam + its make-it-real steps (backend seams + frontend mock flags), the checklist for Phases 4 & 8. diff --git a/dev/post-phase/refinement/refinement-phase-0-bring-up.md b/dev/post-phase/refinement/refinement-phase-0-bring-up.md new file mode 100644 index 0000000..d80c033 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-0-bring-up.md @@ -0,0 +1,171 @@ +# Refinement Phase 0 — Local end-to-end bring-up & the integration seam + +> **Mission:** make the client and the server actually talk to each other, once, on one machine, in a +> repeatable way — and prove it with one real authenticated round-trip. Today the two projects each run, +> but nothing wires them together: the browser can't call the API (no CORS), the API only starts against a +> remote database, and the frontend calls almost nothing real. This phase removes the three hard +> integration blockers and ends with a logged-in session backed by the real backend. +> +> **Track:** integration (both projects) · **Depends on:** nothing · **Unlocks:** every other refinement phase +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context — where this sits + +The 16-phase backend chain (b0–b15) and the 16-phase frontend chain (f0–f15) are both **complete against +their own specs**. But they were built in **parallel, decoupled** lanes that only ever met through the +`dev/contracts/` documents — they were **never run against each other**. As a result the app "runs" but is +not integrated: + +- The frontend is **mock-primary**: 21 of its 22 service domains default to an in-browser mock + (`USE_*_MOCK = true`), so a fresh `npm run dev` calls the backend for **auth only**. Everything you see — + the customer home, search results, bookings — is fake in-memory data. +- The backend has **no CORS**, so even the calls the frontend *does* make (auth) would be blocked by the + browser the moment the two run on different origins. +- The backend only starts against a **committed remote SQL Server** (see the security phase) — there is no + local-database story, so a developer can't stand it up cleanly. + +This phase is the foundation: it makes "run both and watch a real request succeed" a 5-minute, documented +procedure. It does **not** flip the frontend mocks (that's [Refinement Phase 4](refinement-phase-4-frontend-de-mock.md)) +or build any missing endpoints (that's [Refinement Phase 3](refinement-phase-3-contract-batch.md)) — it +only removes the plumbing blockers and proves the seam works with the one domain that's already real: auth. + +**What already exists (do not rebuild):** +- Real, wired phone-OTP auth on both sides — client `services/auth` (`USE_AUTH_MOCK = false`) ↔ server + `AuthController`/`MeController` (`/api/v1/auth/*`, `/api/v1/me`). +- The client fetch layer (`client/src/lib/api/client.ts`) already reads `NEXT_PUBLIC_API_URL`, attaches the + bearer, unwraps the `ApiResult` envelope, and does silent 401 refresh. It is correct — it just has nothing + to talk to. +- On boot the server applies all 17 EF migrations and seeds roles + an admin user + a sandbox gateway + (`Program.cs:99-104`). Migrations are **current**, not behind. + +## 2. Required reading (do this first) + +- **This directory's [README.md](README.md)** — the full problem inventory and how the 10 refinement phases + fit together. Read it before anything. +- `server/src/API/Baya.Web.Api/Program.cs` — the startup pipeline (note: **no `UseCors`**). +- `server/src/API/Baya.Web.Api/appsettings.json` / `appsettings.Development.json` — the connection strings + (identical; both point at the remote `87.107.152.16`). +- `server/src/API/Baya.Web.Api/Properties/launchSettings.json` — the API binds `https://localhost:5002` only. +- `client/src/lib/api/client.ts`, `client/src/config.ts`, `client/.env.development` — the client's API base + URL wiring (`NEXT_PUBLIC_API_URL = https://localhost:5002`). +- `server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/LoggingSmsSender.cs` — OTP delivery is a + **log statement**; the code you need to log in is printed to the server console, not sent by SMS. +- Root [CLAUDE.md](../../../CLAUDE.md) working agreement #6 (never commit secrets) — the local-DB change here + must not add a new committed secret. + +## 3. Scope — build this + +### 3.1 CORS on the server (the #1 blocker) + +There is **no CORS anywhere** in `server/` (verified: zero matches for `AddCors`/`UseCors`). A browser at +`http://localhost:3000` calling `https://localhost:5002` is blocked by the same-origin policy before the +request even reaches a controller. + +- Add a CORS policy via an extension method (per the server's "expose it as an extension method, call it from + `Program.cs`" rule — never inline). Suggested home: + `Baya.WebFramework/ServiceConfiguration/CorsServiceExtension.cs` with `AddCorsPolicies(configuration)`. +- Read the allowed origins from configuration (`Cors:AllowedOrigins` string array), defaulting to + `http://localhost:3000` in Development. **Do not** use `AllowAnyOrigin()` together with credentials. +- The client sends the token as a **bearer header** (not a cookie) on cross-origin calls, so + `AllowCredentials()` is not strictly required — but allow the `Authorization`, `Content-Type`, + `Accept-Language`, and `Idempotency-Key` headers (the client sets all four) and the methods the API uses. +- Register `app.UseCors(...)` in the pipeline **after `UseRouting()` and before `UseRateLimiter()`/ + `UseAuthentication()`** so a pre-flight `OPTIONS` is answered before auth/limits run. +- Keep it config-driven so a deployed environment lists its real web origin(s) and Development stays permissive + to localhost only. + +### 3.2 A local database story (so the API can start without the remote server) + +Today both appsettings files hardcode the remote `87.107.152.16` SQL Server. A developer with no access to +that host cannot boot the API at all. + +- Provide a **local** SQL Server for development. Recommended: a `server/docker-compose.yml` with + `mcr.microsoft.com/mssql/server:2022-latest` (Developer edition, a dev-only `SA_PASSWORD`) exposing `1433`. +- Point the **Development** connection string at that local instance **via a non-committed mechanism** — + `dotnet user-secrets` (the server project already supports it) or environment variables — and replace the + committed remote string in `appsettings*.json` with a placeholder. (The full credential rotation is + [Refinement Phase 5](refinement-phase-5-security-hygiene.md); here we only need a working, secret-free local + default so the app boots.) +- Document the one-time HTTPS dev-cert trust: `dotnet dev-certs https --trust` — without it the browser (and + `fetch`) rejects `https://localhost:5002` and every call fails with an opaque network error. +- On boot the server already runs `MigrateAsync()` + seeders, so pointing at an empty local DB is enough — it + self-migrates and self-seeds roles/admin/gateway on first run. (Rich demo data is + [Refinement Phase 1](refinement-phase-1-database-and-seed.md).) + +### 3.3 A Development-only way to complete OTP login without an SMS gateway + +OTP delivery is `LoggingSmsSender` — it **logs** the code (`server/.../Seams/LoggingSmsSender.cs`) instead of +sending it. A tester can already read the 6-digit code from the server console, but make this explicit and +frictionless: + +- Document the "read the OTP from the server console" procedure in the client and server run docs. +- Optionally (Development/Testing-gated only) return the code in the `request_otp` response **or** expose a + tiny `GET /api/v1/dev/last_otp/{phone}` behind an `IsDevelopment()` guard, so an automated/e2e flow can + complete login. **This must be impossible to enable outside Development** and is superseded by the real SMS + gateway in [Refinement Phase 8](refinement-phase-8-external-rails.md). Do not weaken the `otp` rate-limit or + the per-phone resend window. + +### 3.4 Client env & run docs + +- Confirm `client/.env.development` (auto-loaded by `npm run dev`) has `NEXT_PUBLIC_API_URL = + https://localhost:5002`. It already does — verify it matches the server's actual bound URL. If a developer + runs the API on a different port/scheme, they set a `client/.env.local` override. +- Add a top-level **"Run the whole app locally"** section (root `README.md` or a new + `dev/post-phase/refinement/RUNBOOK.md`) that ties the two `npm`/`dotnet` commands, the cert trust, the local + DB, and the OTP-from-logs step into one copy-pasteable procedure. + +**(DEFERRED here, done later):** flipping any `USE_*_MOCK` flag (Phase 4), building any missing endpoint +(Phase 3), rotating the leaked credentials (Phase 5), a reverse proxy / production CORS origins (Phase 5/9). + +## 4. Mocks & seams in this phase + +No new seams. This phase uses the existing `LoggingSmsSender` (OTP in logs) as the interim OTP channel and +touches no business logic. The only "mock" decision is the optional Development-only OTP echo (3.3), which is +a dev affordance, not a seam. + +## 5. Critical rules you must not get wrong + +- **CORS middleware order matters.** `UseCors` must sit after `UseRouting` and before the rate limiter / auth, + or pre-flight `OPTIONS` requests get rejected (429/401) before the browser ever sends the real request. +- **Do not introduce a new committed secret.** The local DB password and connection string live in + user-secrets/env/compose, never in a committed `appsettings*.json`. Replacing the remote string with a + placeholder is in-scope; committing a new one is not. +- **The Development OTP shortcut is Development-only.** Gate it on `app.Environment.IsDevelopment()` and never + log/return the code in any other environment. It is a bridge until real SMS (Phase 8). +- **Change nothing about the money path, auth crypto, or the envelope shape.** This is plumbing only. + +## 6. Definition of Done + +On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): + +- [ ] `server/`: CORS policy added (config-driven, localhost:3000 in Dev), registered in the correct pipeline + position; `dotnet build Baya.sln` clean, `dotnet test Baya.sln` green. +- [ ] A developer with **no access to the remote server** can boot the API against a local SQL Server using + only documented steps (compose/user-secrets), and the committed appsettings no longer carry a working + remote credential as the default. +- [ ] `dotnet dev-certs https --trust` and the OTP-from-logs step are documented in the runbook. +- [ ] `client/`: `npm run check` green; the client points at the running API; no mock flags changed. +- [ ] The end-to-end proof in §7 passes. + +## 7. How to test (what a human can verify after this phase) + +1. `docker compose up` (or start local SQL Server) → set the Dev connection string via user-secrets. +2. `cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj` → API boots, applies + migrations, seeds, listens on `https://localhost:5002/swagger`. +3. `cd client && npm run dev` → `http://localhost:3000`. +4. Open `http://localhost:3000/fa/login`, enter a phone number, request the OTP. +5. **Read the 6-digit code from the server console** (the `LoggingSmsSender` line), enter it, submit. +6. **Expected:** you land on the customer home; the browser **Network tab shows real calls** to + `https://localhost:5002/api/v1/auth/request_otp`, `/auth/verify_otp`, and `/api/v1/me` all returning + `200` with the `ApiResult` envelope — **and no CORS error in the console**. This is the first real + authenticated round-trip between the two projects. +7. Negative check: temporarily remove the CORS registration and confirm the same flow now fails with a CORS + error in the console — proving the fix is what unblocked it. + +## 8. Hand off & document (close the phase) + +- Update `server/CLAUDE.md` "Startup wiring" to list the new CORS extension in the pipeline order. +- Write/append the local-run runbook (§3.4). +- Report per the operating rules: what was wired, the exact local-run procedure, and confirm auth is the only + real domain until Phase 4. Save a memory note on the CORS + local-DB + OTP-in-logs bring-up so the next + agent doesn't rediscover the blockers. diff --git a/dev/post-phase/refinement/refinement-phase-1-database-and-seed.md b/dev/post-phase/refinement/refinement-phase-1-database-and-seed.md new file mode 100644 index 0000000..42181c8 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-1-database-and-seed.md @@ -0,0 +1,150 @@ +# Refinement Phase 1 — Database: local-dev story, demo seed & migration hygiene + +> **Mission:** turn a fresh database into a *populated marketplace* instead of an empty shell, and give the +> project a clean local-development database story. The 17 EF migrations are current and apply cleanly, but a +> fresh DB seeds only lookup tables + one admin + one gateway — **no nurses, no variants, no search rows, no +> bookings** — so every discovery/search/booking screen is empty on the real path. This phase adds a +> Development-gated demo seed and a repeatable local DB setup. +> +> **Track:** backend (+ DB) · **Depends on:** [Phase 0](refinement-phase-0-bring-up.md) · **Unlocks:** real +> search/discovery/booking data for [Phase 4](refinement-phase-4-frontend-de-mock.md) +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context — where this sits + +**Myth to correct up front:** the migrations are **not "behind."** There are 17 migrations +(`20260628…InitialBaseline` → `20260709…MessagingAndPartnerCenters`) tracking every backend phase b0–b15; the +model snapshot is in sync and the working tree is clean. If a developer *thinks* migrations are behind, it's +because **the target database was never migrated** (they were running the frontend entirely on mocks) — not +because migration files are missing. `Program.cs` calls `MigrateAsync()` on every non-Testing boot, so simply +booting against a fresh DB brings it fully up to date. + +The real database problems are two: + +1. **No local-dev database story.** Both appsettings point at the committed remote SQL Server; there is no + localhost default and no SQLite fallback for `dotnet run`. (Phase 0 introduced the local instance; this + phase makes it the clean, documented default and adds demo data.) +2. **No transactional/domain seed data.** The only seed is reference/lookup tables (31 provinces + cities + + Tehran's 22 districts, the 5-category catalog skeleton, verification step types, review tags, holidays, + cancellation policies, platform config, the invoice-number counter) plus one `admin`/`qw123321` user and + one sandbox ZarinPal gateway. There are **no nurses, customers, profiles, variants, service areas, or + `nurse_search_index` rows**, so `GET /api/v1/search/nurses` returns an empty list even after Phase 3/4 make + the frontend call it for real. + +**What already exists (do not rebuild):** all 17 migrations; the `HasData` reference seeds; the boot-time +`MigrateAsync` + `SeedDefaultUsersAsync` + `SeedPaymentGatewaysAsync`. This phase adds an *additional*, +environment-gated demo seeder — it does not touch the reference seeds or the migration set. + +## 2. Required reading (do this first) + +- `server/src/API/Baya.Web.Api/Program.cs:99-104` — the boot migrate + seed calls. +- `server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs` + — `ApplyMigrationsAsync`, `SeedPaymentGatewaysAsync`, and where a new demo seeder would register. +- `server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs` — the + existing role + admin seed pattern to mirror. +- The catalog seed (`Persistence/Configuration/CatalogConfig/`) and the frontend-backend-gaps note that a + fresh catalog has **categories but no option groups** — the variant builder's required-option step has + nothing to render until an admin authors groups. Decide: seed a representative option-group set, or accept + the empty state until the f15 admin catalog UI is used. +- `product/data-model/` for the entities you'll seed (nurse_profiles, nurse_service_variants, + nurse_service_areas, nurse_search_index, patients, customer_addresses) so the demo world is *valid* per the + business rules (verified nurse → `is_searchable` requires `is_verified=1 AND accepting AND active variant`). +- The b7 search invariant in `server/CLAUDE.md` ("Search & matching") — a demo nurse only appears in search if + the maintainer wrote a `nurse_search_index` row with `is_searchable=1`. + +## 3. Scope — build this + +### 3.1 A clean local database default (finish what Phase 0 started) + +- Ensure `server/docker-compose.yml` (or a documented LocalDB path) gives a one-command local SQL Server. +- The Development connection string resolves from user-secrets/env (not a committed file) to that local + instance; `appsettings*.json` carry only a placeholder. +- Verify `dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence + --startup-project src/API/Baya.Web.Api` works against the local DB, and that booting also self-migrates. + Document both paths (boot-migrate for dev convenience; explicit `ef database update` for the + deploy/migrations-off-boot direction that [Phase 7](refinement-phase-7-unattended-ops.md) will formalise). + +### 3.2 A Development-gated demo seeder (`SeedDemoWorldAsync`) + +A new seeder, invoked from `Program.cs` **only when `app.Environment.IsDevelopment()`** (never in +Production/Staging), that idempotently creates a coherent demo marketplace. It must produce data that satisfies +every downstream invariant so the real-path screens actually populate: + +- **2–3 nurse accounts** (phone-based users with the `nurse` role) each with a `nurse_profiles` row — + at least one **fully verified** (`is_verified=1`, verification `approved`, a verified primary bank account + with `matched_national_id=1`) and one **unverified** (so the trust badge / publish-gate states demo). +- Each verified nurse: **2–4 `nurse_service_variants`** across price units (with valid option answers for the + seeded categories) + **`nurse_service_areas`** in Tehran (some whole-city `district_id=NULL`, some specific + districts) → and therefore **`nurse_search_index` rows with `is_searchable=1`** (drive this through the real + `ISearchIndexMaintainer.RebuildAsync` / the reindex hooks, **not** by hand-inserting index rows, so the + projection stays truthful). +- **1–2 customer accounts** each with a `customer_profiles` row, **1–2 `patients`** (with relation/conditions + once [Phase 3](refinement-phase-3-contract-batch.md) REQ-005 lands — until then, the fields the DTO supports), + and **1–2 `customer_addresses`** with coordinates (so booking + EVV have a real door location). +- **A representative service-option-group set** per category (or a decision to defer to the f15 admin UI) so + the variant builder's required-option step renders on the real path (the frontend-backend-gaps "data gap"). +- *(Optional, if you want the post-payment screens to demo without running the full funnel)* a couple of + **confirmed bookings + sessions** for the demo customer/nurse pairs, mirroring the frontend's seeded booking + ids (5001–5005) so deep-links line up. Keep this optional — the cleaner demo is to create bookings by + actually running the funnel once the frontend is real. + +Idempotency: guard every insert on a stable natural key (phone number, variant option-set hash) so re-running +the seeder on an already-seeded DB is a no-op — the same discipline the existing `SeedPaymentGatewaysAsync` +uses. + +### 3.3 Migration hygiene notes (document, mostly) + +- Confirm no pending model changes (`dotnet ef migrations has-pending-model-changes` or equivalent) — the tree + is clean today; keep it that way. +- Note (do **not** fix here — it's [Phase 7](refinement-phase-7-unattended-ops.md)) that boot-time + `MigrateAsync` races under multi-instance start-up and needs the app login to hold DDL rights; the + deploy-time migration split belongs there. +- The three promised-but-missing forward-dep FKs (`refunds.ticket_id`, `nurse_clawbacks.*_payout_id`, + `invoices.partner_center_id`) are **not** in this phase — they're an additive migration in + [Phase 6](refinement-phase-6-money-correctness.md) alongside the money-correctness work. + +## 4. Mocks & seams in this phase + +None introduced. The demo seeder must go **through the real handlers/maintainer** where an invariant is at +stake (verification flip, search reindex) rather than raw-inserting, so the seeded world is identical to one +built by real usage. + +## 5. Critical rules you must not get wrong + +- **The demo seeder is Development-only.** Gate on `IsDevelopment()`. Seeding fake nurses/customers into a + production DB would be a data-integrity and trust disaster. +- **A nurse only appears in search if `is_searchable=1`** — which requires verified + accepting + an active + variant + a `nurse_search_index` row. Seed the *causes*, let the maintainer compute the index; don't fake + the index. +- **Money stays IRR `BIGINT`; variant prices are digit-strings on the wire.** Seed valid amounts. +- **Idempotent seeding.** Re-running must not duplicate nurses/variants/areas. +- **Don't touch the reference `HasData` seeds or the migration files** — add alongside. + +## 6. Definition of Done + +On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): + +- [ ] A documented one-command local DB + a Development-gated `SeedDemoWorldAsync`; `dotnet build`/`dotnet test` + green; the seeder is idempotent (run twice, same result). +- [ ] Against a fresh local DB, after boot, `GET /api/v1/search/nurses` (via Swagger/curl) returns the seeded + verified nurse(s) with variants — proven **without** any frontend change. +- [ ] The verified demo nurse has a trust badge, variants, and coverage areas; the unverified one does not + surface in search. +- [ ] Migration state confirmed current (no pending model changes) and the local `ef database update` path + documented. + +## 7. How to test (what a human can verify after this phase) + +1. Drop/recreate the local DB, `dotnet run` → observe migrate + reference seed + demo seed in the logs. +2. Swagger → `POST /api/v1/search/nurses` (or the documented search route) with a Tehran city filter → + **expect the seeded verified nurse(s)** with priced variants in the result page. +3. `GET /api/v1/nurses/{id}/trust_badge` for the verified nurse → verified badge; for the unverified → not + verified. +4. Re-run the app → the demo seeder logs "already seeded / no-op" and counts don't double. + +## 8. Hand off & document (close the phase) + +- Update the runbook (Phase 0) with the local DB + demo-seed steps. +- Note in `server/CLAUDE.md` that a Development demo seeder exists and what it creates. +- Report: the exact demo world (ids, phones, which nurse is verified), so the frontend de-mock phase can log in + as those accounts. Save a memory note that migrations are current and the real gap was seed data + local DB. diff --git a/dev/post-phase/refinement/refinement-phase-2-auth-and-role-nav.md b/dev/post-phase/refinement/refinement-phase-2-auth-and-role-nav.md new file mode 100644 index 0000000..bb687dc --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-2-auth-and-role-nav.md @@ -0,0 +1,142 @@ +# Refinement Phase 2 — Auth & role-aware navigation (the "only customer side" fix) + +> **Mission:** fix the most visible symptom — "there are nurse and admin pages, but running the frontend only +> ever shows the customer side." Auth is already real; the reason you only see the customer app is that a +> fresh session has only the `customer` role, role hydration silently falls back to `customer`, and nothing +> routes a nurse/admin to their shell. This phase makes login, role hydration, and role-aware navigation work +> end-to-end for all three actors. +> +> **Track:** integration (mostly frontend + a little backend) · **Depends on:** +> [Phase 0](refinement-phase-0-bring-up.md), [Phase 1](refinement-phase-1-database-and-seed.md) · +> **Unlocks:** reaching the nurse & admin experiences at all +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context — where this sits + +Auth is the **one domain already wired to the real backend** (`USE_AUTH_MOCK = false`). The phone-OTP flow, +session cookies, silent refresh, and `/me` hydration all work. So why is only the customer app visible? + +Diagnosed root causes (all verified in code): + +1. **A fresh phone login has only the `customer` role.** `resolveRoleDestination` + (`client/src/services/auth/routing.ts`) sends a customer to `/` (the customer app lives at the app root — + the `(customer)` route group has no URL segment) and only sends to `/nurse` / `/admin` if `/me.roles` + actually contains `nurse`/`admin`. A brand-new user holds neither until they **self-select the nurse role** + (`POST /api/v1/me/select_role`) or an admin grants a staff role. Nothing in the current run does that, so + everyone lands on the customer app. +2. **A hard-coded `customer` fallback masks un-hydrated / failed role state.** + `client/src/constants/roles.ts` → `DEFAULT_ROLE = customer`; `useActorRole()` + (`client/src/hooks/auth.ts:29`) returns it whenever roles are empty. So if `useSessionRoleSync()` hasn't + hydrated yet, or `/me` errored (backend down), **every private screen resolves to the customer shell** — a + nurse can be silently shown the customer app. +3. **No client-side role guard on the shells.** `(customer)`, `/nurse`, `/admin`, `/partner` layouts each just + wrap their shell; anyone can navigate to any of them (the server still enforces per-endpoint auth, but the + *navigation/chrome* isn't role-aware, so the experience is confusing). +4. **Roles depend entirely on a successful client `/me`.** The server seed + (`client/src/lib/auth/server.ts`) only sets `isAuthenticated` (the JWE is opaque server-side); roles come + only from the client `/me` call in `useSessionRoleSync`. If that call fails, roles never populate. + +**What already exists (do not rebuild):** `resolveRoleDestination`, `RoleRouter`, `SelectRole`, +`useSessionRoleSync`, `useActorRole`, the three shells, and `POST me/select_role`. This phase makes them +*robust and reachable* — it does not rebuild the auth flow. + +## 2. Required reading (do this first) + +- `client/src/services/auth/routing.ts` (`resolveRoleDestination`), `client/src/components/auth/RoleRouter.tsx`, + `client/src/components/auth/SelectRole.tsx`. +- `client/src/services/auth/hooks/useSessionRoleSync.ts`, `client/src/services/auth/hooks/useMe.ts`. +- `client/src/hooks/auth.ts` (`useActorRole`, and the `useAdminCapabilities` roleCodes gating), + `client/src/constants/roles.ts` (`DEFAULT_ROLE`). +- `client/src/app/[locale]/(private-routes)/layout.tsx` (mounts `useSessionRoleSync`) and the four shell + layouts under it (`(customer)`, `nurse`, `admin`, `partner`). +- Server side: `Controllers/V1/MeController` (`/me`, `select_role`) and how `MeResult` exposes `roles` + + `roleCodes`; the role vocabulary in `Domain/Entities/User/RoleNames`. +- REQ-004 in `dev/shared-working-context/frontend/requests/for-backend.md` (the "client owns the active-role + choice" decision to confirm) and REQ-031 (RBAC grant endpoints — needed if you want to grant staff roles + from the UI rather than by seed). + +## 3. Scope — build this + +### 3.1 Make all three actor sessions *reachable* in development + +- **Nurse:** verify the real `POST /api/v1/me/select_role { role: "nurse" }` path — a customer taps "become a + nurse" (the B1 switch / SelectRole) → `/me` now returns `["customer","nurse"]` → `resolveRoleDestination` + routes to `/nurse`. Prove this end-to-end against the real backend. (The [Phase 1](refinement-phase-1-database-and-seed.md) + demo seed can also pre-make nurse-roled phone accounts so you can log straight into `/nurse`.) +- **Admin:** admin sub-roles are **not** self-selectable (`select_role` 403s for them, by design). Provide a + path to an admin session: either the Phase 1 seed grants a demo phone user the `admin` role, or (better, + and it's on the roadmap anyway) deliver REQ-031's `admin_roles/grant_role` so an existing admin can grant + staff roles from `/admin/roles`. Pick one and document it. Confirm `/me` returns the fine-grained + `roleCodes` the admin console's `useAdminCapabilities()` gates on (e.g. `moderation`, `finance`). +- **Partner:** a partner-center admin is a separate authz scope (`useMyPartnerCenter`). Ensure a demo user is + associated with a seeded partner center (Phase 1) so `/partner` resolves instead of access-denied. + +### 3.2 Harden role hydration so it never silently mis-shells + +- Distinguish **"roles not yet loaded"** from **"user has no nurse/admin role."** While `useMe` is in-flight + on a private route, render a neutral loading state (not the customer shell) so a nurse is never flashed the + wrong app. Only fall back to `DEFAULT_ROLE` once `/me` has actually resolved with an empty/customer role set. +- If `/me` **fails** (backend down), surface an explicit "couldn't load your account" state rather than + silently defaulting to customer — otherwise a transient error downgrades a nurse/admin to the customer app. +- Keep `resolveRoleDestination` the single source of truth for "which app" — don't scatter role checks. + +### 3.3 Role-aware navigation guards (chrome, not security) + +- Add a lightweight guard so navigating to a shell the user lacks the role for **redirects** (nurse → `/nurse` + when they hold `nurse`; a pure customer hitting `/nurse` is redirected home with a toast). The server remains + the security boundary; this is UX so the actor experience matches the session. +- For a dual-role user, honor the client-owned intended role (the A1 vs B1 login switch) per REQ-004's + "client owns it" decision, and let them switch actor from the shell. + +### 3.4 Backend confirmations / small adds (coordinate with Phase 3) + +- **REQ-004** (zero code): write the "client owns the active-role choice" decision into the tracker so the + router behavior is contract-blessed. +- If you chose the RBAC route in 3.1: **REQ-031** `admin_roles/list|grant|revoke` (this is the one net-new + backend surface this phase may need; otherwise it's a Phase 1 seed). + +## 4. Mocks & seams in this phase + +None. Auth is already real. Do **not** reach for `USE_AUTH_MOCK = true` to demo roles — that mock's +`MOCK_SCENARIO` toggle is an offline convenience, and using it here would hide the real hydration bug this +phase exists to fix. The whole point is that real `/me` roles drive navigation. + +## 5. Critical rules you must not get wrong + +- **Never treat "roles still loading" as "customer."** That conflation is the core bug — a nurse gets the + customer app for a beat (or forever, if `/me` fails). Gate on a resolved-vs-pending distinction. +- **Admin roles are server-granted, never self-selected.** `select_role` must keep 403-ing for staff roles; + reach admin via seed or RBAC grant, not by loosening `select_role`. +- **Client guards are UX, not security.** Keep every server endpoint's authorization intact; the guard only + redirects the browser to the right shell. +- **i18n both locales** for any new copy (loading/mis-role/redirect states). + +## 6. Definition of Done + +On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): + +- [ ] `npm run check` green; new shared components have `*.test.tsx`; `en.json`/`fa.json` in sync. +- [ ] Logging in as a **nurse-roled** phone user lands on `/nurse`; an **admin** on `/admin`; a **partner** + admin on `/partner`; a plain **customer** on `/`. +- [ ] A customer who selects the nurse role (`select_role`) is routed to `/nurse` after the next `/me`. +- [ ] Navigating to a shell you lack redirects (with a toast), not a broken page. +- [ ] With the backend momentarily stopped, a logged-in nurse sees a loading/error state — **not** the + customer app — and recovers to `/nurse` when `/me` succeeds. + +## 7. How to test (what a human can verify after this phase) + +1. Log in as the seeded **verified nurse** phone account → land on `/nurse`, see the nurse shell + dashboard. +2. Log in as a fresh customer → land on `/` (customer app). Tap "become a nurse" (SelectRole) → confirm + `POST /me/select_role` in the Network tab → after `/me` refetch you're routed to `/nurse`. +3. Log in as the seeded **admin** → land on `/admin`; confirm the sidebar shows only the consoles your + `roleCodes` allow (`useAdminCapabilities`). +4. As a pure customer, manually visit `/nurse` → redirected home with a toast. +5. Stop the API, reload a nurse session → loading/error state (not the customer app); restart → recovers. + +## 8. Hand off & document (close the phase) + +- Update `client/CLAUDE.md` (the auth/role section) to describe the resolved-vs-pending hydration rule and the + role-aware redirects; update the "Project Structure" note if you add a guard component. +- Answer REQ-004 in the tracker; if you built RBAC grant, mark REQ-031 delivered. +- Report the exact demo credentials for each actor and the role-reachability path. Save a memory note that the + "only customer side" symptom was role-reachability + silent `customer` fallback, now fixed. diff --git a/dev/post-phase/refinement/refinement-phase-3-contract-batch.md b/dev/post-phase/refinement/refinement-phase-3-contract-batch.md new file mode 100644 index 0000000..ac4f297 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-3-contract-batch.md @@ -0,0 +1,179 @@ +# Refinement Phase 3 — Backend contract batch (close the 37 REQ gaps) + +> **Mission:** build the missing DTO fields and endpoints the frontend already codes against, so each client +> domain can flip from mock to real. The frontend filed **37 contract requests (REQ-001…037), all still +> `Status: open`** — and 21 of its 22 domains are mock-primary *because* of them. This is the single change +> that unblocks the whole "no API calls to the backend" problem. Almost all of it is **additive** (new nullable +> fields, new read endpoints), so it's low-risk but broad. +> +> **Track:** backend · **Depends on:** [Phase 0](refinement-phase-0-bring-up.md) (nothing hard) · +> **Unlocks:** [Phase 4](refinement-phase-4-frontend-de-mock.md) (each REQ delivered lets one domain flip) +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context — where this sits + +The backend chain is complete against its own specs, but the two lanes were built in parallel and the frontend +consistently discovered small shape gaps: a DTO missing a field it needs to render, or a customer-facing read +that only exists as an admin route. Rather than edit backend code, the frontend recorded each as a **REQ** in +`dev/shared-working-context/frontend/requests/for-backend.md` and shipped a mock that fills the gap, wired so +that **delivering the REQ + flipping one `USE_*_MOCK` flag** swaps to real with no hook/component change. + +This phase delivers those REQs. It is large but coherent — think of it as one backend phase-sized batch of +mostly-additive changes. **It may be run in 2–3 sittings** grouped by the tiers below; deliver a tier, +regenerate swagger, let the frontend flip that tier's domains, repeat. + +The already-done audit `dev/post-phase/server/frontend-backend-gaps.md` reconciled REQ-001…015 against shipped +code (2 done, 1 doc-fix, 12 to build). REQ-016…037 were filed **after** that audit (for the f9–f15 domains) and +are added here. The consolidated table below is the authoritative worklist. + +## 2. Required reading (do this first) + +- **`dev/shared-working-context/frontend/requests/for-backend.md`** — the full REQ-001…037 text (Need / Why / + Proposed shape for each). This is the spec. +- **`dev/post-phase/server/frontend-backend-gaps.md`** — the backend's own REQ-by-REQ reconciliation with + file/line evidence and the priority ordering (REQ-012 first). +- `dev/contracts/domains/*.md` + `dev/contracts/openapi/swagger.v1.json` — the current contract; you'll extend + the DTOs/routes and **regenerate the swagger snapshot** after each tier. +- `dev/contracts/conventions/api-conventions.md` + `money-and-types.md` — the envelope, **snake_case URL + segments**, `pageSize` pagination, IRR-digit-string money rules every new surface must follow. +- The relevant `server/src/Core/Baya.Application/Features//` and `Models/` for each domain you touch. + +## 3. Scope — build this + +Deliver the REQs below. **Zero-code items** are doc/tracker updates. Everything else is an additive DTO field +or a new read/command. Regenerate `dev/contracts/openapi/swagger.v1.json` and flip the REQ's `Status: open → +delivered in refinement-phase-3` as each lands. + +### Tier A — the discovery → request → checkout funnel (do first; highest leverage) + +- **REQ-012 (M) — search enrichment + public profile. DO THIS FIRST.** Add `nurseName`/`avatarUrl` + (+ optional `distanceKm`) to `NurseSearchResultDto` (denormalize into `nurse_search_index` via the maintainer, + or join in `SqlNurseSearch`), and add the aggregated `GET api/v1/nurses/{id}/profile`. Unblocks the entire + discovery funnel (C2/C3) that everything downstream depends on. +- **REQ-005 (S–M) — patient `relation` + `conditions[]`** on `PatientDto` + create/update. +- **REQ-008 (S) — accept client map pin** (`latitude`/`longitude`) on address create/update; store as + `user_pin`, else geocode. Also protects EVV accuracy. +- **REQ-009 (S) — `provinceId`** on `CustomerAddressDto` (join `cities.province_id`). +- **REQ-011 (M) — nurse `credential_details`** command (INO number, specialties) + `isRequired` on + `VerificationStepDto` (stops silent INO/specialty data loss on the real path). +- **REQ-013 (S) — `variantPrice`** (IRR digit-string, + optional `nurseAvatarUrl`) on `BookingRequestDto`. +- **REQ-014 (S) — `variantLabel`** (+ optional `patientAge`) on `BookingRequestListItemDto`. +- **REQ-006 (M) — avatar upload** (`POST …_profiles/avatar`, multipart, via `IObjectStorage`) + `avatarUrl` on + nurse/customer profile DTOs. The **only** item needing multipart; feeds REQ-012/013 avatars. (Pairs with the + real object-storage swap in [Phase 8](refinement-phase-8-external-rails.md), but the endpoint + local-disk + seam work now.) +- **REQ-007 (S) — customer `firstName`/`lastName`/`preferredLanguage`** update. +- **REQ-016 (S–M) — checkout summary** `GET api/v1/booking_requests/checkout_summary/{id}` (served + service/commission/**VAT**/total decomposition that reconciles to the rial). +- **REQ-017 (S) — client-readable payment outcome**: add `bookingId` to `BookingRequestDto` once + `converted` (and/or a `bookings/{requestId}/payments/latest` read) so the confirmation can deep-link and + distinguish declined vs slow callback. +- **REQ-018 (S) — invoice reachable post-capture**: auto-issue the commission invoice on capture (idempotent), + or let the owning customer trigger the idempotent issue on first `GET invoices/{bookingId}`. +- **Auth polish**: **REQ-002 (S)** `codeLength`/`expiresInSeconds` on `RequestOtpResult`; **REQ-003 (S–M)** + optional machine `code` (+`retryAfterSeconds`) on the OTP-failure envelope (the only cross-cutting change — + a small `OperationResult`/`ApiResult` extension). + +### Tier B — refunds, BNPL, payouts, reviews, records (post-booking) + +- **REQ-019/020/021 — customer refunds**: `POST bookings/{id}/cancel` (customer-initiated), `GET + bookings/{id}/cancellation_policy` preview (+ **define the canonical `cancellation_policy_code` set** — the + frontend currently invents `free_24h`/`partial_under_24h`/`customer_no_show`), `GET refunds/by_booking/{id}` + + fee-leg decomposition on the customer status. +- **REQ-022/023/024 — BNPL**: `checkout_bnpl/options/{id}` + `schedule`, eligibility accepting the D3 + `{nationalId, mobile, consent}`, `checkout_bnpl/wallet_installments` + `bookingId` on settled order + + `by_request/{id}`. Also decide the **`balinyaar` in-house provider** (add to the `provider_code` enum or + document how it's modelled). +- **REQ-025 — nurse earnings**: `nurse_payouts/earnings_balance` (four-bucket + **signed** net), + `nurse_payouts/earnings?state=` list, nurse `nurse_payouts/{id}` detail, `failureReason` on the history DTO. +- **REQ-026 — reviews**: `bookings/{id}/review_eligibility` + `bookings/{id}/my_review` reads (+ confirm the + masked-author omission). +- **REQ-027 — family care record**: `patients/{id}/care_record` GET/PUT + `record_access` + structured + `taskResults` on a visit note. **Product decision required first** — is the family-owned record an MVP entity? + (It's not in the current data model.) Confirm before building. + +### Tier C — messaging, notifications, admin, partner consoles + +- **REQ-028 — tickets**: `unreadCount`/`lastMessageAt` on `TicketSummaryDto`, author label, by-booking lookup, + optional `clientMessageId` idempotency on post-message. +- **REQ-029/030/031 — admin**: config `updatedAt`/`updatedBy`; audit filters `actor_id`/`action`/`from`/`to`; + the RBAC `admin_roles/list|grant|revoke` endpoints (coordinate with [Phase 2](refinement-phase-2-auth-and-role-nav.md)). +- **REQ-032/033 — partner center**: `centers/me` + split portal reads + activate/suspend toggle + confirm the + write-then-masked IBAN; center-scoped invoice list + `totalIrr` on `Invoice`. **⚠ Verify route casing:** the + frontend client calls **kebab-case** routes (`admin/partner-centers`, `.../set-active`, `centers/me`) that + violate the `snake_case` URL convention — either the b15 controllers already diverge, or the frontend guessed + wrong. Pin the real routes in swagger and reconcile before the frontend flips `USE_PARTNER_MOCK`. +- **REQ-034 — verification admin**: nurse-level queue (server returns per-step today), on-demand signed + document URL, whole-verification approve/reject. +- **REQ-035 — refund admin**: refund **preview** + explicit approve/reject (the single POST currently + creates+executes). +- **REQ-036 — payout admin**: single preview (eligible + skipped + processingDate), `holidayShifted` flag, + `transfer_reference` record route. +- **REQ-037 — reviews admin**: `tagCodes[]` on `ModerationQueueItemDto`. + +### Zero-code (do alongside every tier) + +- **REQ-010 — pagination doc sweep**: the server binds camelCase **`pageSize`**; sweep the `page_size` + occurrences in `dev/contracts/domains/*.md` to `pageSize`. +- **REQ-001, 004, 015 — written confirmations**: envelope/casing/pagination (done), client-owned active role + (Phase 2), status enum + `checkInAddressMatch` tri-state (verified). Write them into the tracker. +- Reconcile the **fee/VAT-rate drift** the frontend mock carries (0.15 fee / 0.10 VAT vs the "12% / 10%" + described elsewhere) — the served `checkout_summary` (REQ-016) is the authority; make the config values it + reads canonical and note them so the frontend stops assuming. + +## 4. Mocks & seams in this phase + +- REQ-006 introduces the **first multipart endpoint** over `IObjectStorage` — the local-disk mock is fine now; + the presigned-URL/S3 swap is [Phase 8](refinement-phase-8-external-rails.md). Keep the endpoint contract + (multipart in, stored URL out) stable across the swap. +- No new external seams otherwise — these are DB/DTO reads and commands over existing tables. + +## 5. Critical rules you must not get wrong + +- **Additive, not breaking.** New fields are nullable/optional; don't change existing field names/casing/shape + (the frontend types are already generated against the current swagger). +- **Follow the conventions exactly**: `snake_case` URL segments, `pageSize` pagination, `ApiResult` envelope, + **IRR money as digit-strings**, stable snake_case enum codes. A new route that breaks a convention is worse + than a missing route — the frontend guessed the convention, not the exception. +- **Money-free stays money-free**: REQ-013's `variantPrice` is a *display rate*, not an engagement total — the + booking_request still stores no money. +- **Server owns the numbers**: commission/VAT/refund %/eligibility/holiday-shift are computed server-side and + served; the client renders, never recomputes. REQ-016/020/025/036 must serve reconciling decompositions. +- **Regenerate `swagger.v1.json` after each tier** and flip the REQ statuses — a stale contract re-breaks the + frontend types. + +## 6. Definition of Done + +On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): + +- [ ] Every REQ above is either delivered (endpoint/field shipped + tested) or explicitly answered (zero-code + confirmation / deferred with a written reason) in `for-backend.md`; **no REQ still reads `Status: open`** + without a note. +- [ ] `dotnet build Baya.sln` zero new warnings; `dotnet test Baya.sln` green, including tests for the new + reads/commands (tenancy 404s, reconciling decompositions, idempotency where relevant). +- [ ] `dev/contracts/openapi/swagger.v1.json` regenerated; `dev/contracts/domains/*.md` updated (incl. the + `page_size`→`pageSize` sweep and the canonical `cancellation_policy_code`/BNPL-provider enums). +- [ ] The partner-center route casing is pinned in swagger and matches (or the frontend is told to adjust). + +## 7. How to test (what a human can verify after this phase) + +Per tier, via Swagger/curl against the [Phase 1](refinement-phase-1-database-and-seed.md) demo data: +- Tier A: `POST search/nurses` returns rows **with `nurseName`/`avatarUrl`**; `GET nurses/{id}/profile` + aggregates profile+variants+badge+latest review; `GET booking_requests/checkout_summary/{id}` returns a + breakdown where `service + commission + vat = total`. +- Tier B: a customer `POST bookings/{id}/cancel` creates a refund; `GET bookings/{id}/cancellation_policy` + previews the tier + per-session refundability; `nurse_payouts/earnings_balance` returns four buckets + a + signed net. +- Tier C: `centers/me` resolves the caller's own center; the admin queues/previews return their shapes; RBAC + grant/revoke works. + +Each success here is what lets the matching frontend domain flip in [Phase 4](refinement-phase-4-frontend-de-mock.md). + +## 8. Hand off & document (close the phase) + +- Update every touched `dev/contracts/domains/*.md`, regenerate swagger, and flip all REQ statuses. +- Update the mocks-registry "Frontend client-side mocks" rows: each delivered REQ moves its domain toward + flippable. +- Report which REQs landed per tier so the frontend lane knows exactly which `USE_*_MOCK` flags it can flip. + Save memory notes on any non-obvious decision (canonical cancellation codes, the `balinyaar` provider + decision, the family-care-record product call). diff --git a/dev/post-phase/refinement/refinement-phase-4-frontend-de-mock.md b/dev/post-phase/refinement/refinement-phase-4-frontend-de-mock.md new file mode 100644 index 0000000..83bf9be --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-4-frontend-de-mock.md @@ -0,0 +1,152 @@ +# Refinement Phase 4 — Frontend de-mock (flip every domain to the real backend) + +> **Mission:** make the frontend actually call the backend. Today 21 of 22 client service domains default to an +> in-browser mock (`USE_*_MOCK = true`), so the running app is almost entirely fake data. Each domain's real +> `clientApi` already exists and maps the live routes 1:1 — the swap is a **one-line flag flip per domain**, +> once that domain's REQ gaps ([Phase 3](refinement-phase-3-contract-batch.md)) are delivered and the demo data +> ([Phase 1](refinement-phase-1-database-and-seed.md)) exists. This phase flips them, in dependency order, and +> verifies each against the real API. +> +> **Track:** frontend · **Depends on:** [Phase 1](refinement-phase-1-database-and-seed.md), +> [Phase 2](refinement-phase-2-auth-and-role-nav.md), [Phase 3](refinement-phase-3-contract-batch.md) · +> **Unlocks:** a genuinely integrated app +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context — where this sits + +The frontend was deliberately built **mock-primary** so each phase could ship before its backend counterpart +merged. Every domain follows the same seam: `services/{domain}/apis/index.ts` picks `mockApi` vs `clientApi` +from a compile-time `USE_{DOMAIN}_MOCK` constant in `constants.ts`; the `clientApi` already wraps `clientFetch` +and unwraps the `ApiResult` envelope. The design intent, recorded in every phase report and the mocks-registry, +is: **deliver the REQ → flip the flag → the domain is real, with no hook/component change.** + +This phase executes that flip for every domain. The order matters because the domains form a funnel — a domain +whose *inputs* are still mocked can't meaningfully go real (e.g. `bookingRequests` needs `search`/`patients`/ +`addresses` real first). Auth is already real and is the template. + +**Note on flags:** the `USE_*_MOCK` constants are **hard-coded literals**, not env vars. Flipping means editing +the `constants.ts` line to `false`. Consider (optional, small) refactoring each to read +`process.env.NEXT_PUBLIC_USE_{DOMAIN}_MOCK` with a `false` default so environments can differ without code +edits — but the minimum deliverable is the flags flipped to real. + +## 2. Required reading (do this first) + +- **`dev/shared-working-context/reports/mocks-registry.md`** → the "Frontend client-side mocks" table. Every + row lists the flag, the file, and the exact "Make it real →" precondition (which REQ + which upstream domain). + This is your checklist. +- `client/CLAUDE.md` → "services/" section (the per-domain descriptions + their REQ gaps). +- `client/src/services/auth/` → the already-real template for how a flipped domain behaves. +- Each domain's `apis/clientApi.ts` (verify the routes it maps against the regenerated + `dev/contracts/openapi/swagger.v1.json` — especially the ones Phase 3 flagged: partner-center **kebab-case** + routes, BNPL `provider_code`, cancellation-policy codes). +- The [Phase 3](refinement-phase-3-contract-batch.md) hand-off (which REQs actually landed) — only flip a + domain whose gaps are delivered. + +## 3. Scope — flip these, in this order + +Flip each `USE_*_MOCK` to `false` **only after its precondition holds**, then run the domain's screens against +the real backend and fix any real-path shape surprises. Group order mirrors the dependency funnel: + +### Group 1 — identity & reference (few/no gaps) +- `USE_GEOGRAPHY_MOCK` — reference lookups; b4 live, **no REQ needed**. Flip early. +- `USE_PATIENTS_MOCK` — needs REQ-005 (relation/conditions). +- `USE_PROFILES_MOCK` — needs REQ-006 (avatar) + REQ-007 (name/language). `uploadAvatar` currently throws 501 + until REQ-006. +- `USE_NURSE_BANK_MOCK` — b3 live, **no REQ**. Flip. +- `USE_ADDRESSES_MOCK` — needs REQ-008 (pin) + REQ-009 (provinceId). +- `USE_SERVICE_AREAS_MOCK` — b4 live, **no REQ**. Flip. + +### Group 2 — catalog, verification, search (the discovery funnel) +- `USE_CATALOG_MOCK` — b5 live; **data caveat**: a fresh backend has categories but **no option groups** until + Phase 1 seeds them (or the f15 admin authors them). Flip only once option groups exist, or the variant + builder's required-option step is empty. +- `USE_VERIFICATION_MOCK` — needs REQ-011 (credential_details) + REQ-034 (admin queue/doc-URL/approve). +- `USE_SEARCH_MOCK` — needs REQ-012 (name/avatar/distance + public profile). **The linchpin of the funnel.** + +### Group 3 — booking funnel +- `USE_BOOKING_REQUESTS_MOCK` — needs REQ-013/014 **and** search/patients/addresses real (Groups 1–2). +- `USE_BOOKINGS_MOCK` — a booking only exists after a paid request converts, so needs booking-requests + + payment real. Also set `NEXT_PUBLIC_EVV_MOCK_GPS=off` to use real `navigator.geolocation`. +- `USE_PAYMENT_MOCK` — needs REQ-016 (checkout summary) + REQ-017 (outcome/bookingId) + REQ-018 (invoice) and + the upstream request flow real. Delete the dev mock-gateway harness page once flipped. + +### Group 4 — money reversal & post-booking +- `USE_REFUNDS_MOCK` — needs REQ-019/020/021 (+ canonical cancellation codes) + REQ-035 (admin). +- `USE_BNPL_MOCK` — needs REQ-022/023/024 (+ the `balinyaar` provider decision) + upstream real. Delete the + BNPL gateway harness page once flipped. +- `USE_PAYOUTS_MOCK` — needs REQ-025 (+ REQ-036 admin). +- `USE_REVIEWS_MOCK` — needs REQ-026 (+ REQ-037 admin `tagCodes`). +- `USE_PATIENT_RECORDS_MOCK` — the nurse visit-note half is already real (b14); the family-record half needs + REQ-027 (**and the product decision** on whether it's a real entity). Flip only the parts that are backed; + keep the family record mocked if the product defers it. + +### Group 5 — comms & admin consoles +- `USE_TICKETS_MOCK` — needs REQ-028 + bookings real. +- `USE_NOTIFICATIONS_MOCK` — b1 live; real once upstream domains actually **dispatch** notifications (the + mock exists because nothing dispatches while upstreams are mocked). Flip after Groups 3–4. +- `USE_ADMIN_MOCK` — needs REQ-029/030/031. +- `USE_PARTNER_MOCK` — needs REQ-032/033 **and** the route-casing reconciliation (verify the kebab-case routes + bind before flipping). + +### Cleanup +- Remove the dev-only test harness pages once their domains are real: the payment mock-gateway + (`…/checkout/gateway/page.tsx`) and the BNPL provider-handoff (`…/checkout/bnpl/gateway/page.tsx`) — on the + real path the PSP/provider `redirectUrl` is an absolute URL, so these are never linked. +- Remove the dev-only `__mock*` helpers (`__mockApproveAll`, `__mockPublishSubmittedReview`, + `__mockPushNotification`, etc.) as their domains go real, or keep them behind a dev guard if still useful. + +## 4. Mocks & seams in this phase + +This phase *retires* mocks. The one client seam that legitimately stays is `ILocationProvider` (EVV GPS +capture) — set `NEXT_PUBLIC_EVV_MOCK_GPS=off` to select the real `navigator.geolocation`. The +`AddressMapPicker` remains a canvas stand-in until a real map widget is inlined (that's a separate design task, +not a data mock). + +## 5. Critical rules you must not get wrong + +- **Don't flip a domain whose inputs are still mocked** — you'll get empty screens or shape errors. Follow the + group order; a domain is only real if the whole chain beneath it is. +- **Verify each `clientApi`'s routes against the regenerated swagger before flipping** — Phase 3 flagged three + concrete casing/enum mismatches (partner-center kebab-case, BNPL `balinyaar`, cancellation codes). A wrong + route silently 404s. +- **The flip is a flag, not a rewrite.** If a screen needs a hook/component change to work on the real path, + that's a Phase 3 gap that wasn't fully delivered — file/finish the REQ, don't hack the component. +- **`npm run check` and `test:ci` stay green** after each flip; keep `en.json`/`fa.json` in sync (no new + strings expected, but empty/error states may surface copy that was never exercised on the mock path). +- **Real error/empty states now matter.** The mocks always returned tidy data; real endpoints 404/return empty. + Verify each domain's empty, error, and loading states against the real backend — this is where mock-only apps + break. + +## 6. Definition of Done + +On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): + +- [ ] Every domain whose REQ preconditions are met has `USE_*_MOCK = false` and is verified working against the + real backend; any domain still mocked is because its REQ/product decision is explicitly outstanding + (documented, not forgotten). +- [ ] `npm run check` + `npm run test:ci` green; translations in sync. +- [ ] The full happy path runs on real data end-to-end: log in → browse categories → search → open a nurse + profile → request a booking → nurse accepts → checkout → confirmation → booking detail → review. +- [ ] Dev test-harness pages + `__mock*` helpers for real domains removed or dev-guarded. +- [ ] `client/CLAUDE.md` and the mocks-registry updated to reflect which domains are now real. + +## 7. How to test (what a human can verify after this phase) + +1. With client + server running on real data (Phases 0–3 done), open the app and confirm the **Network tab + shows real `/api/v1/*` calls for every screen** (not just auth) — no domain is serving in-browser mock data. +2. Walk the customer funnel: home category grid → search (real seeded nurses) → nurse profile → request → + (switch to the nurse account) accept → (customer) checkout → pay (mock gateway is gone; on a real PSP this + is [Phase 8](refinement-phase-8-external-rails.md), but the request→booking conversion works) → confirmation + → booking detail. +3. Nurse funnel: verification flow, services builder, coverage, requests inbox, visits/EVV, earnings — all on + real data. +4. Admin: verification queue, refunds, payouts, moderation, config/holidays/audit, partner centers — on real + data with role-gated nav (Phase 2). +5. Confirm empty/error states render correctly where the demo data is sparse (e.g. a nurse with no reviews). + +## 8. Hand off & document (close the phase) + +- Update `client/CLAUDE.md` "services/" notes and `dev/shared-working-context/reports/mocks-registry.md` + (frontend mocks table) to mark each flipped domain real. +- Report the flip status of all 22 domains (real / still-mocked-because-X) and any real-path surprises found. + Save a memory note on the de-mock order and any domain that needed a Phase-3 follow-up. diff --git a/dev/post-phase/refinement/refinement-phase-5-security-hygiene.md b/dev/post-phase/refinement/refinement-phase-5-security-hygiene.md new file mode 100644 index 0000000..2e13b26 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-5-security-hygiene.md @@ -0,0 +1,90 @@ +# Refinement Phase 5 — Security & config hygiene (deployment blockers) + +> **Mission:** remove the committed secrets and dev-grade defaults that block any non-local deployment. None of +> this changes behavior, but two items are **live credential leaks sitting in git today**. Do the credential +> rotation (5.1) *now*, independently of everything else. +> +> **Track:** backend (config/security) · **Depends on:** nothing (can run in parallel with Phases 0–4) · +> **Unlocks:** any real/shared-environment deployment +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context + +This phase **is** the already-written server audit's **post-phase-1**. It is reproduced here as a refinement +phase for sequencing; the full evidence (file/line for every item) and the exact fixes live in +**[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-1**. Read that +section — it is the spec. This file only orders and frames it. + +The findings, in short (all verified in code, dated 2026-07-10): + +1. **Committed `sa` connection string** — a real remote SQL Server (public IP `87.107.152.16`, plaintext + password) in `appsettings.json` **and** `appsettings.Development.json` (byte-identical), for both the app DB + and the log DB. Anyone with repo access owns the database. Violates root `CLAUDE.md` agreement #6. +2. **Placeholder JWE signing/encryption + PII field-encryption keys** (`local-dev-…-change-me`), a ~7-day + access-token lifetime (`ExpirationMinutes: 10000`), `RequireHttpsMetadata = false`, and stub + `Issuer`/`Audience` (`MyWebsite`). +3. **Seeded `admin` / `qw123321`** full-admin user on every non-Testing boot. +4. **Auto-seeded active sandbox ZarinPal gateway** on every boot (a production DB would silently hold an active + sandbox money gateway). +5. **Kestrel HTTP/2-only default** (breaks non-TLS HTTP/1.1 hops). +6. **Rate limiter not proxy-aware** (`RemoteIpAddress`, no `ForwardedHeaders`) → behind a proxy every client + shares one bucket; plus the two payment webhooks use mismatched rate policies. + +## 2. Required reading + +- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-1 (items + 1.1–1.6) — the authoritative, file/line-cited spec. +- **[../server/runtime-services.md](../server/runtime-services.md)** § 1–3 (SQL Server + reverse-proxy notes). +- [Phase 0](refinement-phase-0-bring-up.md) — it already introduced a secret-free local DB default; this phase + finishes the rotation and the deployed-environment story. + +## 3. Scope — deliver plan items 1.1–1.6 + +- **5.1 (do first, today) — rotate & remove the committed `sa` connection strings.** Assume compromised: rotate + the password, create a least-privilege app login, move both connection strings to user-secrets (dev) / env + vars (deploy), commit only a placeholder, and add a secret-scanning pre-commit hook. Consider history + scrubbing (`git filter-repo`). +- **5.2 — replace the placeholder JWE + field-encryption keys** with per-environment secrets; set a sane + access-token lifetime (≤ 60 min; refresh already exists); `RequireHttpsMetadata = true` outside Development; + real `Issuer`/`Audience`. (Rotating the field key needs a re-encryption migration — do it before real PII + exists.) +- **5.3 — environment-gate the seeded admin** (read bootstrap creds from config; Development-only; force a + password change on first login). +- **5.4 — environment-gate the sandbox gateway seed** (Development/Testing only, or seed `is_active = false`). +- **5.5 — fix the Kestrel HTTP/2-only default** (`Http1AndHttp2`; give gRPC its own endpoint if kept). +- **5.6 — make rate limiting proxy-aware** (`ForwardedHeaders` trusting the known proxy; partition on the + resolved client IP) and pick one deliberate webhook rate policy. + +## 4. Mocks & seams + +None. Config/security only. + +## 5. Critical rules + +- **5.1 is urgent and independent** — it doesn't wait for this phase's slot; the credential is leaking now. +- Rotating the field-encryption key **invalidates existing dev-DB ciphertext** — acceptable pre-launch, but do + it before real PII exists and plan the re-encryption migration if any real data exists. +- Don't break local dev: Development keeps working defaults (via user-secrets/env), only the *committed* values + become placeholders. + +## 6. Definition of Done + +- [ ] No working secret remains in any committed file (connection strings, JWE keys, field keys, admin + password, gateway creds) — verified by a secret scan. +- [ ] Access-token lifetime sane; `RequireHttpsMetadata` gated by environment; real issuer/audience. +- [ ] Admin + sandbox-gateway seeds are Development-gated (or config-driven). +- [ ] Kestrel default is `Http1AndHttp2`; rate limiter honors forwarded headers; webhooks use one deliberate + policy. `dotnet build`/`dotnet test` green. + +## 7. How to test + +- Fresh clone with no user-secrets → the app **fails fast with a clear "missing connection string" error** + (not a silent connect to a leaked remote). With user-secrets/env set → boots normally. +- Confirm no `admin`/`qw123321` and no active sandbox gateway appear in a non-Development boot. +- Behind a reverse proxy, distinct client IPs get distinct rate-limit buckets. + +## 8. Hand off & document + +- Update `server/CLAUDE.md` (startup wiring / identity / rate-limiting) to reflect the config-driven secrets and + the forwarded-headers middleware. Record the credential rotation in the security log / handoff. Save a memory + note that the committed `sa` string was rotated and externalized. diff --git a/dev/post-phase/refinement/refinement-phase-6-money-correctness.md b/dev/post-phase/refinement/refinement-phase-6-money-correctness.md new file mode 100644 index 0000000..c1a1c9f --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-6-money-correctness.md @@ -0,0 +1,92 @@ +# Refinement Phase 6 — Money-path correctness completion + +> **Mission:** close the one genuine money-correctness hole and the referential-integrity gaps left around the +> (verified-clean) ledger. The load-bearing invariants — balanced ledger groups, the four money DB CHECKs, +> webhook idempotency, forward-only status machines — are all verified in code. This phase fixes the holes +> *around* them. +> +> **Track:** backend (money path) · **Depends on:** nothing hard (do **before** any real BNPL/manual refund — +> i.e. before [Phase 8](refinement-phase-8-external-rails.md) money rails) · **Unlocks:** ledger⇄bank +> reconciliation +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context + +This phase **is** the server audit's **post-phase-2**. Full file/line evidence and fixes: +**[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-2**. Read it — it is +the spec. + +The headline is one real defect: **the BNPL/manual refund settlement path is unreachable.** A card refund posts +its `refund_payable ↔ escrow_held` clearing immediately, but a BNPL-revert / manual-bank refund is left in +`processing` with the clearing "deferred to reconciliation" — and **no reconciliation path exists anywhere**: +`Refund.MarkSucceededAsync` has **zero callers**, no endpoint/webhook/job performs `processing → succeeded`. +So every BNPL/manual refund permanently overstates `escrow_held` and strands `refund_payable`; the ledger will +never reconcile with the bank. This must be fixed before real BNPL/manual refunds exist. + +## 2. Required reading + +- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-2 (items + 2.1–2.6) — the spec, with every file/line. +- `server/CLAUDE.md` "Refunds, clawbacks & invoices" + "Payments core" — the invariants you must preserve. +- The [Phase 3](refinement-phase-3-contract-batch.md) refund REQs (019/020/021/035) — coordinate: the customer + cancel command + admin preview/approve you may build there interact with the settlement fix here. + +## 3. Scope — deliver plan items 2.1–2.6 + +- **6.1 (top fix) — wire the unreachable refund settlement.** Add a `ConfirmRefundSettlementCommand` (admin + `POST admin_refunds/{id}/confirm_settlement` + a BNPL-callback branch when the provider confirms customer + cash-back) that transitions `processing → succeeded`, stamps `settled_at`, and posts + `LedgerPosting.RefundPayableClearing` in the same commit; plus a `mark_failed` counterpart. Tests for both + channels. +- **6.2 — add the promised forward-dep FKs** (one additive migration + an index): `refunds.ticket_id`, + `nurse_clawbacks.original_payout_id` / `recovered_in_payout_id`, `invoices.partner_center_id` (+ index). The + target tables all shipped and the values are wired; only the constraints are missing. Fix the now-false config + comments too. +- **6.3 — extend `IAuditable`** to the admin-decided money & trust entities (`Refund`, `NurseClawback`, + `NursePayout`, `NursePayoutBatch`, `NurseVerification`) so approve/reject/process and the `is_verified` flip + leave an audit-diff row. Confirm `[AuditRedacted]` covers `iban_snapshot` first. +- **6.4 — close the refund channel-execute-before-commit crash window** (persist a `pending` refund row before + the external channel call, then execute and update — the two-phase intent/confirm shape the webhook handler + already uses). +- **6.5 — test the untested admin money paths** (`WriteOffClawbackCommand` / bad-debt group; a true racing + webhook-insert; messaging `is_internal` handler-level tests). +- **6.6 — retire the orphaned `refund_ticket_required` config key** (b15 superseded it) or repurpose it; fix the + false description. + +## 4. Mocks & seams + +None new. This is domain/ledger code over existing seams; the BNPL/PSP mocks stay until +[Phase 8](refinement-phase-8-external-rails.md). + +## 5. Critical rules + +- **The ledger stays append-only and balanced.** Every new posting is a balanced group under one + `transaction_group_id`; the settlement clearing must reconcile (`Σdebit = Σcredit`). +- **Idempotency preserved.** The settlement confirm and the crash-window fix must be safe to replay (the + idempotency key + status machine are the backstop). +- **Do 6.1 before real BNPL/manual refunds** ([Phase 8](refinement-phase-8-external-rails.md)) — otherwise real + money strands ledger state. +- Don't regress the four DB CHECKs (`CK_Bookings_AmountSplit`, `CK_NursePayouts_NetSplit`, + `CK_Refunds_LegSplit`, `CK_BnplTransactions_SettleSplit`). + +## 6. Definition of Done + +- [ ] A BNPL/manual refund can reach `succeeded` (via admin confirm + BNPL callback) and posts the + `refund_payable ↔ escrow_held` clearing — proven by a test that the ledger reconciles. +- [ ] The three forward-dep FK sets + the invoice index exist (additive migration); config comments corrected. +- [ ] The five listed entities are `IAuditable` and produce audit-diff rows on admin decisions. +- [ ] The refund crash-window is closed; the previously-untested admin money paths have tests. +- [ ] `dotnet build` zero new warnings; `dotnet test` green (incl. the new tests). + +## 7. How to test + +- Create a BNPL refund → it lands `processing` → admin `confirm_settlement` → status `succeeded`, `settled_at` + set, and `GetNursePayableBalance`/escrow reconcile (no stranded `refund_payable`). +- `WriteOffClawback` posts a balanced `bad_debt` group; a replayed settlement confirm is an idempotent no-op. +- Inspect `audit_logs` after an admin refund approve / a verification `is_verified` flip → a diff row exists. + +## 8. Hand off & document + +- Update `server/CLAUDE.md` refunds/payments sections + the contract docs for the new admin refund route. Flip + the mocks-registry / audit notes for the settlement seam. Save a memory note that the unreachable BNPL/manual + refund clearing is now wired (the audit's top code fix). diff --git a/dev/post-phase/refinement/refinement-phase-7-unattended-ops.md b/dev/post-phase/refinement/refinement-phase-7-unattended-ops.md new file mode 100644 index 0000000..c10f335 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-7-unattended-ops.md @@ -0,0 +1,103 @@ +# Refinement Phase 7 — Unattended operation: scheduler, locking & multi-instance readiness + +> **Mission:** make the platform run itself. Today only two recurring jobs exist; the **weekly payout batch, +> credential-expiry scan, EVV no-show sweep, and Moadian reconciliation are admin-click-only** while their +> cadence config keys sit unread — so **nurses are not paid unless an operator clicks**. This phase adds a real +> scheduler, and (only when you need more than one API instance) shared cache + lock, and splits migrations from +> boot. +> +> **This is where "do I need Redis / Elasticsearch?" gets answered: no, not for a single-instance MVP.** Redis +> becomes necessary the moment you run a **second** API instance (shared cache invalidation + cross-instance +> money lock). Elasticsearch is **never** MVP — SQL search is real and correct +> ([Phase 9](refinement-phase-9-observability-and-scale.md) covers the deferred ES path). +> +> **Track:** backend (infra) · **Depends on:** [Phase 6](refinement-phase-6-money-correctness.md) (6.1 +> settlement is a reconciliation job here) · **Unlocks:** unattended ops; >1 API instance +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context + +This phase **is** the server audit's **post-phase-4**. Full evidence & fixes: +**[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-4** and the topology +in **[../server/runtime-services.md](../server/runtime-services.md)** § 5, 7. + +Verified state today: +- Two in-process `PeriodicTimer` hosted services only: booking-request expiry (1 min) + notification retention + (24 h). **No `IJobScheduler` interface actually exists** (the registry name is aspirational); **no + Hangfire/Quartz** package is referenced. +- The verification credential-expiry scan, EVV no-show sweep, weekly **payout batch generation**, and Moadian + reconciliation are **admin-manual endpoints** whose seeded cadence keys + (`verification_expiry_scan_cadence_hours`, `no_show_scan_cadence_hours`, `nurse_payout_interval_days`) are + never read on a schedule. +- `ICacheService` = in-process `MemoryCache`; `IDistributedLock` = per-key `SemaphoreSlim` (one process only). + The DB uniques/state-machines are the money-correctness backstop either way, but cache + lock **silently + degrade** the moment a second instance runs. +- Migrations run on boot (`MigrateAsync`) → concurrent multi-node start-up races on DDL; the app login needs + permanent DDL rights. + +## 2. Required reading + +- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-4 (items + 4.1–4.3). +- **[../server/runtime-services.md](../server/runtime-services.md)** § 5 (Redis), § 7 (job scheduler), + Deployment notes 1 & 4. +- The mocks-registry rows for `IJobScheduler`, `ICacheService`, `IDistributedLock`. + +## 3. Scope — deliver plan items 4.1–4.3 + +- **7.1 — real job scheduler + register the deferred crons.** Adopt Hangfire (SQL Server storage — **no new + infra**) or Quartz; re-home the two existing sweeps and add recurring jobs for: credential-expiry scan, EVV + no-show sweep, **weekly payout-batch generation**, [Phase 6](refinement-phase-6-money-correctness.md)'s refund + settlement reconciliation, and [Phase 8](refinement-phase-8-external-rails.md)'s Moadian poll — each reading + its seeded cadence key. **Keep the admin manual triggers as overrides.** Money-*moving* payout processing can + stay human-approved — schedule *generation*, keep `process` manual until trust is earned. Jobs are already + idempotent by design. +- **7.2 — Redis for `ICacheService` + `IDistributedLock` (only when scaling past one instance).** Add + `StackExchange.Redis`; `RedisCacheService` (same key/TTL scheme) + `RedisDistributedLock` (SET NX PX + + token-checked release, lease ≥ the longest money handler), config-selected registration. **Single-instance + MVP does not need this** — document it as the gate for horizontal scaling; keep DB uniques authoritative. +- **7.3 — separate migrations from boot** (multi-instance + least privilege). A deploy-time migration step + (`dotnet ef database update` in CI, or a `--migrate` one-shot mode) + a boot-time schema *check* instead of an + apply; seeders become idempotent deploy steps. (Local dev can keep boot-migrate for convenience — gate the + behavior by environment.) + +## 4. Mocks & seams + +- The two hosted services move behind the real scheduler; the `IJobScheduler` registry row becomes "recurring + jobs (Hangfire/Quartz)". +- Redis is introduced **only if 7.2 is in scope for your deployment** — otherwise the in-process cache/lock + stay and this is documented as the scale-out gate. + +## 5. Critical rules + +- **Jobs must stay idempotent** (they are) — a scheduler retry must never double-pay or double-post. +- **Keep payout *processing* human-approved at first** — schedule generation, not money movement, until trust + is earned. +- **Don't add Redis "because."** It's required only for >1 instance; adding it single-instance is complexity + without payoff. Same for ES (never MVP). +- **The DB uniques remain the correctness backstop** even with a real distributed lock. + +## 6. Definition of Done + +- [ ] A real scheduler runs the two existing sweeps + the four deferred crons (each reading its cadence key); + admin manual triggers still work as overrides. +- [ ] Payout *generation* is scheduled; *processing* remains an explicit admin action. +- [ ] (If scaling) Redis-backed cache + lock behind a config switch, with the DB uniques still authoritative; + otherwise the single-instance limitation is documented as the scale-out gate. +- [ ] Migration-on-boot is environment-gated; a deploy-time migration path exists. `dotnet build`/`dotnet test` + green. + +## 7. How to test + +- Set a short cadence in config → observe the payout-batch-generation / expiry-scan / no-show jobs fire on + schedule (logs) and produce the same result as the admin manual trigger. +- (If Redis) run two API instances against one Redis → a cache invalidation on instance A is seen by B, and the + money lock is held across both. +- A one-shot `--migrate` (or CI step) applies migrations; a normal boot only *checks* schema. + +## 8. Hand off & document + +- Update `server/CLAUDE.md` startup wiring (scheduler) + the mocks-registry rows. Update + [../server/runtime-services.md](../server/runtime-services.md) if the deployment topology changes (scheduler + storage, optional Redis). Save a memory note: Redis = scale-out gate, ES = never MVP, scheduler now runs the + deferred crons. diff --git a/dev/post-phase/refinement/refinement-phase-8-external-rails.md b/dev/post-phase/refinement/refinement-phase-8-external-rails.md new file mode 100644 index 0000000..7b15520 --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-8-external-rails.md @@ -0,0 +1,111 @@ +# Refinement Phase 8 — External rails go real (SMS → trust/identity → money) + +> **Mission:** swap the in-process mocks for real vendors, in impact order. **`ISmsSender` is launch-critical — +> OTP delivery is a log statement today, so no real user can ever log in.** After that: identity/KYC/geocoding/ +> object-storage, then the money rails (PSP, BNPL, payout, tax). Each swap is an *adapter behind an existing +> seam*, not a redesign — the seam shapes (idempotency keys, server-side re-verify, upsert-first webhooks) are +> already the handler behavior. +> +> **Track:** backend (integrations) · **Depends on:** [Phase 6](refinement-phase-6-money-correctness.md) (do 6.1 +> before real BNPL/manual refunds), [Phase 7](refinement-phase-7-unattended-ops.md) (scheduler for the +> reconciliation/Moadian polls) · **Unlocks:** a real, transacting platform +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context + +This phase **is** the server audit's **post-phase-5 (trust rails)** + **post-phase-6 (money rails)**. Full +per-seam "make it real" steps live in **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** +§ post-phase-5/6, the topology in **[../server/runtime-services.md](../server/runtime-services.md)** § 8–16, and +the exact swap recipe (package → options → implement → register) for every seam in +**[../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md)**. + +Every vendor dependency is a deterministic in-process mock today. This phase makes them real, one seam at a +time, config-selected — handlers don't change. + +## 2. Required reading + +- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-5 (5.1–5.6) and + § post-phase-6 (6.1–6.6). These are the spec. +- **[../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md)** + — the per-row make-it-real steps (packages, config keys, the exact methods to implement). +- **[../server/runtime-services.md](../server/runtime-services.md)** § 8–16 — vendor defaults + deployment + notes per rail. + +## 3. Scope — swap the seams, in impact order + +### 3.1 Trust & identity rails (post-phase-5) +- **5.1 — real SMS gateway behind `ISmsSender` (LAUNCH-CRITICAL, do first).** Kavenegar/Ghasedak/SMS.ir; + template/pattern OTP send; `Seams:Sms:{ApiKey,SenderLine,BaseUrl}`. This **replaces the Development OTP-in-logs + bridge** from [Phase 0](refinement-phase-0-bring-up.md). Keep the per-phone resend window + `otp` rate policy. + **No real user can log in until this ships.** +- **5.2 — real Shahkar + e-KYC** (`IShahkarVerifier`, `IIdentityKycProvider`) via a Finnotech-class bridge; keep + shared-SIM a handled failure; persist real vendor refs. +- **5.3 — real استعلام شبا** (`IBankAccountOwnershipVerifier`) — the b13 first-payout gate; bundle with 5.2's + vendor family. +- **5.4 — real geocoder** (`IGeocoder`) behind Neshan (address coordinates / EVV distance). REQ-008's user pin + reduces but doesn't remove the need. +- **5.5 — real object storage** (`IObjectStorage`) — MinIO/S3/ArvanCloud with presigned PUT/GET; makes the b6 + signed-URL contract and REQ-006 avatars real (local disk + `file://` today). +- **5.6 — document manual MoH/INO/eNamad** (`ICredentialVerifier`, `ILicenseVerificationService`) as the + intended MVP state (no public B2B API exists) — mark the registry rows so they stop reading as debt. + +### 3.2 Money rails (post-phase-6) — **do [Phase 6](refinement-phase-6-money-correctness.md) 6.1 first** +- **6.1 — real PSP/IPG + webhook signatures + تسهیم** (`IPaymentProvider`, `IWebhookVerifier`, + `ISettlementSplitProvider`) — ZarinPal/Sadad/Vandar/Jibit; mandatory server-side `verify` re-check; per-provider + HMAC on the raw body; provider factory per gateway row; persist full responses. (High risk — Shaparak + certification lead time.) +- **6.2 — real BNPL adapters** (`IBnplProvider`/`IBnplProviderResolver`) — SnappPay/Digipay; Toman↔IRR only via + `ICurrencyNormalizer`; per-contract commission from the settle response. (Do Phase 6 §6.1 revert-clearing + first.) Also settle the **`balinyaar` in-house provider** decision from [Phase 3](refinement-phase-3-contract-batch.md) REQ-022. +- **6.3 — real PAYA/SATNA payout rail** (`IBankTransferProvider`) + the async `submitted → paid/failed` + reconciliation callback (the mock settles instantly). Wire the weekly trigger via + [Phase 7](refinement-phase-7-unattended-ops.md). +- **6.4 — remove `IPaymentCaptureSimulator`** (b10's real capture superseded it; move it into the test host or + gate to Development/Testing). +- **6.5 — real Moadian submission + reconciliation** (`IMoadianClient`) — enrollment + signing cert; the + `pending → submitted → registered/failed` poll registered under [Phase 7](refinement-phase-7-unattended-ops.md). +- **6.6 — decide the partner-center settlement rail** (resolver exists; no money path pays a center yet) — a + product decision, then optionally a center-settlement ledger account + payout reusing the b13 machinery. + +## 4. Mocks & seams + +This phase retires the vendor mocks one row at a time (each config-selected, so a partial rollout is fine — +e.g. real SMS + real geocoder while payments stay mocked in a pre-launch environment). The manual MoH/INO/eNamad +seams (5.6) **stay** as the intended MVP mechanism. + +## 5. Critical rules + +- **5.1 SMS is the gate to a first real session** — sequence it first; nothing else matters if no one can log + in. Once it ships, **the OTP must never be logged** (remove the Development log/echo bridge). +- **Never trust a payment callback alone** — the mandatory server-side `verify` re-check + per-provider HMAC are + non-negotiable on the real PSP swap. +- **Money-rail swaps are irreversible-transfer territory** — the DB uniques (`UNIQUE(booking_id)` payout link, + filtered payment uniques), forward-only status machines, and idempotency keys are the backstops; keep them. +- **Do [Phase 6](refinement-phase-6-money-correctness.md) 6.1 before real BNPL/manual refunds** or real money + strands ledger state. +- **Currency conversion happens only in the adapter** via `ICurrencyNormalizer` — never internally. + +## 6. Definition of Done + +- [ ] Real SMS delivers OTPs to a real handset; the Development OTP-in-logs/echo bridge is removed; a real user + can complete login end-to-end. +- [ ] Each swapped seam is config-selected, persists real vendor responses, and its handler is unchanged; + `dotnet build`/`dotnet test` green. +- [ ] Money rails: a real card capture confirms via server-side verify + signature; a real payout reconciles + `submitted → paid`; an invoice registers with Moadian; the BNPL revert-clearing (Phase 6) completes. +- [ ] `IPaymentCaptureSimulator` is out of the production registration. +- [ ] The registry rows for swapped seams are 🟢; manual-review seams (5.6) are marked "manual = intended MVP". + +## 7. How to test + +- Request an OTP → receive a real SMS → log in (no code in the logs). +- (Sandbox) run a real card payment → server-side verify + signature check pass → booking converts; a signed + webhook with a bad signature is rejected. +- Run a payout batch against the real rail → it moves to `submitted`, then the async callback flips it to + `paid`; an invoice reaches `registered` at Moadian. + +## 8. Hand off & document + +- Update `server/CLAUDE.md` (seam registrations), the mocks-registry (rows → 🟢), and + [../server/runtime-services.md](../server/runtime-services.md) (which services are now live). Record vendor + accounts/config keys in the deploy docs (never the secrets). Save a memory note per rail that goes live. diff --git a/dev/post-phase/refinement/refinement-phase-9-observability-and-scale.md b/dev/post-phase/refinement/refinement-phase-9-observability-and-scale.md new file mode 100644 index 0000000..d67904e --- /dev/null +++ b/dev/post-phase/refinement/refinement-phase-9-observability-and-scale.md @@ -0,0 +1,94 @@ +# Refinement Phase 9 — Observability, ops hardening, docs honesty & scale-later + +> **Mission:** make the running platform diagnosable and honest, and record the explicitly-deferred scale work +> so nobody mistakes it for missing MVP scope. This is the "finish before launch, then keep for later" bucket. +> +> **Track:** backend (observability/docs) + explicit deferrals · **Depends on:** nothing hard (start anytime; +> finish the observability items before launch) · **Unlocks:** production diagnosability +> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** + +## 1. Context + +This phase **is** the server audit's **post-phase-7 (observability & ops hardening)** + **post-phase-8 (scale & +later)**. Full evidence & fixes: +**[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-7/8**. + +## 2. Required reading + +- **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-7 (7.1–7.6) and + § post-phase-8 (8.1–8.5). +- **[../server/runtime-services.md](../server/runtime-services.md)** § 4 (Prometheus), § 17 (Elasticsearch — + deliberately later). + +## 3. Scope + +### Observability & ops hardening (post-phase-7 — finish before launch) +- **9.1 — add tracing + consolidate the two metric stacks.** OTel is metrics-only (no `WithTracing`/OTLP); two + overlapping Prometheus stacks run. Add `WithTracing` (AspNetCore + EF) exporting OTLP; pick **one** metrics + stack; wire trace-id into `ApiResult.requestId` for support correlation. +- **9.2 — broaden health checks + split readiness/liveness.** The single check is app-DB only; add log-DB, + object-storage (write probe), Redis (when [Phase 7](refinement-phase-7-unattended-ops.md) lands); expose + `/healthz/live` vs `/healthz/ready`; remove the dead `currentUrl` line. +- **9.3 — revisit prod log levels + notification channels.** Deployed envs write **Warning+ only** (every + Information-level audit trail dropped); raise to Information+ with retention (or a file/OTLP sink); ensure + **no PII is logged** (the SMS OTP log disappears with [Phase 8](refinement-phase-8-external-rails.md) 5.1); + delete the dead Elasticsearch sink block + package or revive it deliberately. +- **9.4 — audit-log growth & archival.** `audit_logs` is append-only with no retention (and Phase 6 §6.3 grows + it); add a retention/archival policy as a [Phase 7](refinement-phase-7-unattended-ops.md) job; define legal + retention for money/verification rows first. +- **9.5 — decide `TicketMessage.Body` encryption + the gRPC plugin's fate.** Ticket bodies are the refund/dispute + paper trail (users type phone numbers, addresses, clinical detail) and are plaintext with no documented + decision — recommend encrypting via the existing converter pattern. The gRPC plugin duplicates only the + OTP/token flow, forces the HTTP/2 posture, and enables reflection unconditionally — remove it or give it a + dedicated HTTP/2 endpoint + disable reflection outside Development. +- **9.6 — keep the docs honest.** Prune the stale mocks-registry duplicate rows, rename the `IJobScheduler` row + to "recurring jobs", mark delivered/answered REQs, note `IPaymentCaptureSimulator`'s removal. (Root `CLAUDE.md` + rule 7 — stale instructions are worse than none.) + +### Scale & later (post-phase-8 — explicitly NOT MVP; record, don't build) +- **9.7 — Elasticsearch read backend + outbox feeder.** `SqlNurseSearch` is real and correct; `Search:Backend` + fails fast on any non-`sql` value. Build `ElasticNurseSearch` + the outbox/CDC feeder **only when SQL search + shows strain**. Not now. +- **9.8 — analytics pipeline.** `IAnalyticsSink` writes `ops.SystemEvents` fire-and-forget; pipe to a + warehouse/stream when product needs it. +- **9.9 — holiday-calendar feed.** The table is manually maintained; a yearly ops-checklist item is an + acceptable alternative to a lunar-Hijri drift feed. +- **9.10 — push/SMS notification channels.** `InAppNotificationDispatcher` drops non-InApp channels; add + fan-out (SMS via [Phase 8](refinement-phase-8-external-rails.md) 5.1's sender, FCM push) when the UX demands. +- **9.11 — deferred product tables** (`organizations`, `organization_nurses`, `fraud_flags`, + `recurring_booking_schedules`, `bnpl_settlement_entries`, availability slots, customer national-ID KYC, geo + bulk import) — all verified absent and all pure additive migrations when product pulls them. Leave documented. + +## 4. Mocks & seams + +None new. 9.7/9.10 are the make-it-real steps for the `INurseSearch` (Elastic backend) and +`INotificationDispatcher` (SMS/push channels) rows — deferred by design. + +## 5. Critical rules + +- **No PII in logs** — verify after 9.3 (the OTP log must be gone; clinical text and IBANs are never logged). +- **Deferrals are documented, not silent** — 9.7–9.11 stay explicitly out of MVP with a written "when to pull + it" trigger, so a future reader doesn't mistake them for gaps. +- **Don't build Elasticsearch/analytics/push "because"** — each has a concrete trigger (search strain, product + need, UX demand). Until then, SQL search / in-app notifications are the real, correct MVP. + +## 6. Definition of Done + +- [ ] Tracing exports OTLP; one metrics stack; `requestId` carries the trace id. +- [ ] Health checks cover the real dependencies with `/healthz/live` vs `/healthz/ready`. +- [ ] Prod logs Information+ with retention and **no PII**; the dead ES sink is resolved. +- [ ] Audit retention policy exists; ticket-body encryption + gRPC decisions are made and documented. +- [ ] The mocks-registry / REQ tracker / architecture maps are honest and current. +- [ ] 9.7–9.11 are recorded as deferred with explicit pull-triggers. `dotnet build`/`dotnet test` green. + +## 7. How to test + +- A request produces a trace with a `requestId` that matches the `ApiResult.requestId` in the response. +- `/healthz/ready` fails when a dependency (object storage / Redis) is down; `/healthz/live` stays up. +- Grep the logs after an OTP + a booking + a payout → no phone number, OTP, IBAN, or clinical text appears. + +## 8. Hand off & document + +- Update `server/CLAUDE.md` (observability wiring, ticket encryption, gRPC decision), the mocks-registry, and + the REQ tracker. Update [../server/runtime-services.md](../server/runtime-services.md) for any new + observability service. Save a memory note capturing the encryption/gRPC decisions and the deferral triggers.