add refinement phases
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/<Area>/` 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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user