cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -0,0 +1,59 @@
# The hardening loop prompt
Two ways to drive the chain. Both are self-terminating: when nothing is left unchecked, the agent
reports "chain complete" and stops instead of doing work.
---
## A. One phase per session (recommended)
Paste this into a **fresh** Claude Code session, once per iteration:
```
Read dev/post-phase/hardening/README.md and dev/post-phase/hardening/issues.md.
Find the FIRST phase (0→5) whose issues.md items are not all checked off, and whose dependencies
(per the README chain table) are already complete. Execute that phase's file
(dev/post-phase/hardening/hardening-phase-N-*.md) end to end:
1. Do the phase's required reading first. Do not re-audit findings — issues.md evidence is verified;
re-locate line numbers if they drifted.
2. Implement the full scope. Stay inside the phase's track (client/ or server/) unless the file says
otherwise.
3. Run that project's gate (client: npm run check + npm run test:ci · server: dotnet build Baya.sln +
dotnet test Baya.sln) and the phase's own Definition of Done — including any runtime probe it
demands. A phase is not done on a code-only fix if its DoD requires observed behavior.
4. Tick the finished items in issues.md (add the commit hash), write the phase report, update the
docs the phase names, and commit with message "hardening phase N: <title>".
5. End your reply with: the phase completed, what's verified, anything re-deferred (and why), and
which phase the loop should run next. If ALL items in issues.md are checked, say "HARDENING CHAIN
COMPLETE" and summarize the end state instead.
```
Repeat until it says complete. Phases 0 (frontend) and 1 (backend) may run in two parallel sessions —
they touch disjoint trees; everything else runs one at a time.
## B. Unattended interval loop (Claude Code /loop)
If you want it self-driving in one long session:
```
/loop Execute the next incomplete hardening phase per dev/post-phase/hardening/LOOP-PROMPT.md
section A (steps 15). One phase per iteration, commit when green. If issues.md is fully checked,
say "HARDENING CHAIN COMPLETE" and stop the loop.
```
Caveats for unattended mode: Phase 0's DoD needs the dev server + API running (keep them up, or let
the agent start them); Phase 4 involves product decisions (`cancellation_policy_code` set, REQ-027) —
the agent will decide from `product/` docs, so review that commit; nothing in the chain should push,
only commit locally.
## Guardrails (both modes)
- **Never skip a DoD runtime check.** H-01 survived nine phases precisely because "the code is
correct" was accepted without a live probe.
- **issues.md is the single progress state.** No side lists; re-deferrals get written there, not
dropped.
- If a phase file contradicts the live code (something already fixed, a file moved), trust the code,
note the drift in the report, and keep going — don't restore the file's assumption.
- One phase = one commit (Phase 4 may commit per REQ group). Don't batch phases into one commit.
+125
View File
@@ -0,0 +1,125 @@
# Hardening phases — auth gate, role enforcement & the last mocks
**Created:** 2026-07-16 · **Scope:** whole repo (client + server), after the 16+16 build phases and the
10 refinement phases all completed · **Method:** a 6-dimension multi-agent audit of the live code
(anonymous access, role guarding, server authorization, remaining mocks, end-to-end journey walk,
session/error UX), every finding **adversarially re-verified** against the code with file:line evidence,
plus a live runtime probe of the dev server.
This directory is a **runnable chain of 6 hardening phases**. Run them **in order, one at a time**,
pointing a fresh agent at one phase file (*"Execute `dev/post-phase/hardening/hardening-phase-0-auth-gate.md`
end to end"*) — or drive the whole chain with [`LOOP-PROMPT.md`](LOOP-PROMPT.md). The full verified
finding ledger (with per-item checkboxes) is in [`issues.md`](issues.md).
> **Why this exists after the refinement chain.** The refinement phases made the two projects run as one
> app. But live probing shows the user-facing symptom *"the app never asks me to log in, and roles feel
> unenforced"* is real — caused by an auth gate that never executes at runtime, a token-liveness check
> that can't read the server's encrypted token, an admin RBAC policy that is structurally dead (every
> admin endpoint 403s), and two client money-path mocks left dangling by the phase-4 de-mock.
---
## The headline: why the app never asks for login
Three defects **mask each other**, which is why this survived nine refinement phases:
1. **`client/middleware.ts` never executes in the running app** (verified live: a cookie-less
`GET /fa/admin` returns 200 with page HTML; a bare `GET /` returns 404 instead of next-intl's locale
redirect — under both Turbopack *and* webpack). The gate code itself is correct. Root cause on this
machine: a stray `C:\Users\Lenovo\pnpm-lock.yaml` (home directory, not in the repo) makes Next.js
infer the **workspace root as the home directory**, so the middleware file is never bound. The repo
must defend against this (pin the root in `next.config.mjs`) and prove the gate runs (runtime DoD in
Phase 0). Next 16 has also deprecated `middleware.ts` in favor of `proxy.ts`.
2. **`isTokenAlive` can never return `true` for a real token.** The server's access token is an
**encrypted JWE** (`JwtService.cs:116-128`, `EncryptingCredentials`, A128KW/A128CBC-HS256); the client
helper (`client/src/lib/auth/token.ts:24-38`) base64-decodes segment 1 and JSON-parses it — impossible
for a JWE. So had the middleware ever run, it would have **redirect-looped logged-in users to /login**.
Same helper seeds server-side auth state (`getServerAuthState`), so `isAuthenticated` seeds `false` on
every hard reload. The wire already returns `accessExpiresAt`/`refreshExpiresAt`
(`client/src/services/auth/types.ts:65-66`) — the fix is a readable companion expiry cookie.
3. **The client fallback can't rescue an anonymous visitor.** `useMe()` is enabled only when
`isAuthenticated`; for a cookie-less request that's `false` forever, so `useRoleHydration()` stays
`loading` and every private shell renders an **infinite branded splash** instead of redirecting to
login. RoleGuard needs an explicit unauthenticated → redirect-to-login branch (defense in depth).
**One manual step no phase can do for you:** delete or move the stray `C:\Users\Lenovo\pnpm-lock.yaml`
from your home directory (it is unrelated to this repo). Phase 0 pins the workspace root so the app no
longer *depends* on that cleanup, but the stray file will keep confusing other tools too.
---
## What's actually fine (don't re-fix)
- The middleware/auth-gate **logic** and `PUBLIC_PATHS` are correct as written — the problem is execution
+ the JWE check, not the design.
- The 4 private shells genuinely all wrap `RoleGuard`; admin **mutations** are consistently gated behind
`useAdminCapabilities()`.
- Server **tenancy** is enforced correctly in every spot-checked handler (bookings, tickets, patients,
care records, bank accounts, centers) — owner-or-staff checks with 404-not-403.
- Public endpoints (catalog/geo/search/nurses, webhooks, dev) are intentionally anonymous;
`dev/last_otp` correctly 404s outside Development.
- The demo seed gives Journey A a real searchable Tehran nurse; base route names match 1:1
client↔server; the silent-refresh mechanism (single-flight, retry-once) is sound.
## The verified problem inventory (17 findings + root cause)
Severity-ordered; the full ledger with evidence is [`issues.md`](issues.md).
| # | Severity | Problem | Phase |
| --- | --- | --- | --- |
| H-01 | blocker | Auth gate never executes at runtime (workspace-root misdetection; middleware deprecated) | 0 |
| H-02 | blocker | `isTokenAlive` can't read the JWE token → would redirect-loop; seeds `isAuthenticated=false` on reload | 0 |
| H-03 | high | Anonymous visitor to a private shell gets an infinite splash, never a login prompt | 0 |
| H-04 | blocker | `DynamicPermission` RBAC is dead: no RoleClaim ever seeded/grantable → every admin endpoint 403s for the seeded `super_admin`/`finance` personas | 1 |
| H-05 | high | `BookingRoles.Admin` bundles Support/Moderation into clinical-notes + nurse-balance + forced-transition access | 1 |
| H-06 | blocker | Refunds mock cross-imports the retired bookings-mock store → real cancellations 404 | 2 |
| H-07 | blocker | BNPL mock cross-imports retired mock stores → installment checkout 404s or fabricates a fake success while the real request expires unpaid | 2 |
| H-08 | blocker | Verification 100% mocked while catalog/search are real → a real nurse "publishes" services that can never appear in search, no feedback | 2 |
| H-09 | high | Nurse earnings screen fabricated although the REQ-025 endpoints are live (flag held hostage by the admin half of the seam) | 2 |
| H-10 | medium | Payment outcome hard-codes `bookingId: null` though REQ-017 is delivered → confirmation deep links lost | 2 |
| H-11 | high | Logout/login never clear the React Query cache → previous user's data leaks to the next login on the same device | 3 |
| H-12 | high | The only error boundary dumps a raw English stack trace, no retry; no `error.tsx`/`global-error.tsx` | 3 |
| H-13 | high | All 401/403/5xx/network toasts are hardcoded English on a Persian-default app | 3 |
| H-14 | medium | Admin read-only consoles (audit/verification/tickets/roles) render without a capability check — only the nav hides them | 3 |
| H-15 | medium | Tier B/C contract REQs still open (refunds 019-021, BNPL 022-024, admin 029-031, partner 032/033/038, verification admin 034, refund admin 035, payout admin 036) | 4 |
| H-16 | medium | Partner portal unreachable from login (no `/me` signal), fully mocked, no tenancy gate on its pages | 4+5 |
| H-17 | medium | patientRecords family record: client `string` ids vs wire `long` ids → PUT is write-unsafe; edits don't survive reload | 5 |
## The 6 hardening phases
| # | Phase | Track | Fixes | Depends on |
| --- | --- | --- | --- | --- |
| **0** | [Auth gate & session liveness](hardening-phase-0-auth-gate.md) | frontend | H-01 H-02 H-03 | — |
| **1** | [Admin RBAC & staff role scopes](hardening-phase-1-admin-rbac.md) | backend | H-04 H-05 (+delivers REQ-031) | — |
| **2** | [Money-path mock integrity](hardening-phase-2-mock-integrity.md) | frontend | H-06 H-07 H-08 H-09 H-10 | 0 |
| **3** | [Session & error-surface hardening](hardening-phase-3-session-error-ux.md) | frontend | H-11 H-12 H-13 H-14 | 0 |
| **4** | [Contract completion batch (Tier B/C)](hardening-phase-4-contract-completion.md) | backend | H-15 H-16(server half) | 1 |
| **5** | [Final de-mock & partner reachability](hardening-phase-5-final-demock.md) | frontend | H-16(client half) H-17 + flip the last flags | 2, 4 |
```
frontend: 0 auth gate ──► 2 mock integrity ──► 5 final de-mock
└───────► 3 session/error UX ▲
backend: 1 admin RBAC ──► 4 contract batch ──────┘
```
Phases 0 and 1 are independent — a frontend and a backend agent can run them in parallel
(the [shared-working-context protocol](../../shared-working-context/README.md) applies).
**Minimum path to "the app asks for login and roles hold":** 0 → 1. **Minimum path to "the money
path works end-to-end on real data":** 0 → 2. Everything real, no mocks: all six.
## How the phase files are written
Same skeleton as the rest of the repo (the [phase template](../../phases/_shared/phase-template.md)):
mission, context, required reading, enumerated scope **with the audit's file:line evidence inlined**
(so the executing agent doesn't re-audit), invariants, Definition of Done, how to test, close-out.
Before executing any phase, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).
## Related documents
- [issues.md](issues.md) — the verified finding ledger (evidence + checkboxes; the loop's progress state).
- [LOOP-PROMPT.md](LOOP-PROMPT.md) — the reusable prompt that drives this chain phase by phase.
- [../refinement/README.md](../refinement/README.md) — the prior chain this one follows.
- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
— the REQ ledger Phase 4 closes out.
- [../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md)
— the mock registry Phases 2 & 5 update.
@@ -0,0 +1,106 @@
# Hardening Phase 0 — Auth gate & session liveness (make the app ask for login)
> Make the login gate actually execute at runtime, make token liveness readable despite the JWE access
> token, and give anonymous visitors a login redirect instead of an infinite splash. This is the direct
> fix for the reported symptom *"there is no ask for logging in."*
> **Track:** frontend · **Depends on:** — · **Unlocks:** Phases 2, 3
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-01, H-02, H-03** from [issues.md](issues.md). Three defects mask each other: the middleware
never runs (so nobody is asked to log in), its token check could never pass anyway (the token is an
encrypted JWE), and the client fallback shows an infinite splash to anonymous visitors. The gate
*design* (middleware + `PUBLIC_PATHS` + RoleGuard-as-chrome) is correct and stays.
**What already exists (do not rebuild):** `client/middleware.ts` (correct logic), `PUBLIC_PATHS`
(`src/constants/routes.ts:169`), `RoleGuard`/`useRoleHydration`/`AuthAccountError`
(refinement-phase-2), `persistAuthTokens`/`clearAuthTokens` (`src/lib/auth/session.ts`), the
single-flight silent refresh (`src/lib/api/refresh.ts`), and the wire's
`accessExpiresAt`/`refreshExpiresAt` (`src/services/auth/types.ts:62-66`).
## 2. Required reading (do this first)
- [issues.md](issues.md) H-01/H-02/H-03 — the verified evidence; don't re-audit.
- `client/middleware.ts`, `client/src/lib/auth/token.ts`, `client/src/lib/auth/session.ts`,
`client/src/lib/auth/server.ts`, `client/src/components/auth/RoleGuard.tsx`,
`client/src/services/auth/hooks/useRoleHydration.ts`, `client/src/services/auth/routing.ts`.
- `client/CLAUDE.md` → "Auth Cookies & session state" (the documented design you are repairing) and
"Golden rules".
- Next.js 16 `proxy.ts` file convention (middleware is deprecated/renamed; file must sit at the same
level as `app` — for this repo that's `client/src/`).
## 3. Scope — build this
1. **Make the gate execute — and prove it.**
- Pin the workspace root in `client/next.config.mjs`: `turbopack.root` (and
`outputFileTracingRoot`) → the `client/` directory, so a stray lockfile in an ancestor directory
(the live failure: `C:\Users\Lenovo\pnpm-lock.yaml`) can never re-point root detection.
- Migrate `client/middleware.ts``client/src/proxy.ts` per the Next 16 convention (export
`proxy`; keep the exact logic + matcher). Update the `client/CLAUDE.md` references.
- **Runtime proof is part of the deliverable:** with the dev server running and no cookies,
`GET /` must 307 to `/fa`, and `GET /fa`, `/fa/bookings`, `/fa/nurse`, `/fa/admin` must each
redirect to `/fa/login`. `GET /fa/login` must 200. If the probe fails, keep working — do not
declare done on a code-only fix.
2. **JWE-compatible liveness.** The access token can't be decoded client-side, but verify_otp/refresh
already return `accessExpiresAt`/`refreshExpiresAt`:
- `persistAuthTokens` additionally writes a **non-sensitive companion cookie** (e.g.
`access_expires_at`, name in `COOKIE_NAMES`) holding the ISO/epoch expiry; cookie `maxAge`s for
both tokens derive from the served expiries (this also fixes the documented 7d-vs-server drift
for the refresh cookie). `clearAuthTokens` deletes it.
- Replace `isTokenAlive(token)` call sites (proxy + `getServerAuthState`) with a check that reads
the expiry cookie when the token doesn't decode as a plain JWT (keep the JWT path as fallback so
a future JWS still works). Token **presence** without a readable, live expiry = not alive.
- Result: `getServerAuthState` seeds `isAuthenticated=true` after a hard reload of a logged-in
session — verify `useMe`/`useSessionRoleSync` then hydrate roles normally.
3. **Unauthenticated branch in the client fallback (defense in depth).**
- `useRoleHydration` gains an explicit `unauthenticated` state (auth context says logged out);
`RoleGuard` redirects it to `/${locale}/login?next=<current path>` instead of splashing forever.
- The login flow honors `next`: after verify (+ role routing), `RoleRouter` prefers a safe,
same-origin relative `next` path over `resolveRoleDestination` when present. Never redirect to
an absolute/external URL.
- `clientFetch`'s unrecoverable-401 redirect should also carry `next`.
## 4. Mocks & seams in this phase
None introduced. Do not touch `USE_*_MOCK` flags here (Phase 2 owns them).
## 5. Critical rules you must not get wrong
- **The middleware/proxy stays UX-only** — no signature verification client-side; the API remains the
authority. Don't try to decrypt the JWE in the client.
- The expiry cookie is a **liveness hint, not a credential** — never gate real authorization on it.
- `PUBLIC_PATHS` uses `startsWith` — never add `'/'` or another prefix-of-everything entry.
- Don't break the locale flow: the i18n redirect handling and locale header in the current middleware
must survive the migration exactly.
- Keep RoleGuard's existing loading/error/mismatch semantics (refinement-phase-2) — you're adding a
fourth branch, not rewriting the guard.
- Golden rules: no hardcoded strings (new copy → both `messages/*.json`), cookie access only via the
cookie manager, constants for names/params.
## 6. Definition of Done
- The runtime probe in §3.1 passes (paste the curl/status output into your report).
- Logged-in user: hard reload on `/fa/bookings` stays there (no splash-hang, no bounce to login).
- Logged-out user: any private deep link → login → completes OTP → lands back on the deep link.
- Logout → immediately bounced to login on the next private navigation.
- `npm run check` green; `npm run test:ci` green; RoleGuard + routing tests extended for the
`unauthenticated` branch and the `next` param.
## 7. How to test
1. `cd client && npm run dev` (server up per `dev/post-phase/refinement/RUNBOOK.md`).
2. No cookies: `curl -I http://localhost:3000/fa/bookings` → 307/308 `Location: /fa/login?next=…`.
3. Log in (dev OTP via `GET /api/v1/dev/last_otp/{phone}`), F5 on the home shell → stays, roles load.
4. Delete only the `access_token` cookie, navigate → silent refresh recovers; delete both → login ask.
5. Visit `/fa/nurse` as a customer-only session → RoleGuard redirect toast (unchanged behavior).
## 8. Hand off & document
- Update `client/CLAUDE.md` (middleware→proxy path, expiry-cookie lifecycle, the JWE note) — its
"middleware.ts" references and cookie table must match reality.
- Tick H-01/H-02/H-03 in [issues.md](issues.md) with the commit hash.
- Write `dev/shared-working-context/reports/hardening-phase-0-report.md` (what was verified at
runtime, any surprises).
- **Note to the human:** the stray `C:\Users\Lenovo\pnpm-lock.yaml` should still be deleted manually;
the root pin makes the app immune, but other tools may not be.
@@ -0,0 +1,112 @@
# Hardening Phase 1 — Admin RBAC & staff role scopes (make role checks real)
> Resurrect the dead `DynamicPermission` policy so the seeded admin personas can actually operate the
> backoffice, expose the already-built role-management handlers over HTTP (delivering REQ-031), and
> narrow the over-broad `BookingRoles.Admin` bucket that hands Support/Moderation staff clinical and
> financial access. This is the server half of *"roles aren't checked."*
> **Track:** backend · **Depends on:** — (parallel-safe with Phase 0) · **Unlocks:** Phase 4
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-04, H-05** from [issues.md](issues.md). Every admin endpoint is gated by
`[Authorize(ConstantPolicies.DynamicPermission)]`, whose handler passes only for the literal role
`"admin"` or a matching RoleClaim — but no user holds `"admin"`, no RoleClaim is ever seeded, and the
only claim-writing service has no controller. Net effect: the seeded `super_admin` (09120000020) and
`finance` (09120000021) personas — which RUNBOOK.md documents as the sanctioned admin path — get 403 on
**every** admin console action. Separately, handler-level role sets are the entire authz boundary for
bookings (controllers carry blanket `[Authorize]`), and `BookingRoles.Admin` bundles five staff roles
into clinical-notes/nurse-balance/forced-transition access, violating the codebase's own
"narrowest fitting scope" convention.
**What already exists (do not rebuild):** the `DynamicPermissionHandler`/`DynamicPermissionService`
wiring (`ServiceCollectionExtension.cs:49-50,100-103`), `RoleManagerService.ChangeRolePermissionsAsync`
(`RoleManagerService.cs:145-208` — the claim writer), the Application-layer
`Features/{Role,Admin}` commands/queries (handlers exist, no HTTP surface), the demo seeder
(`DemoWorldSeeder.cs:279-308`), tenancy checks in handlers (correct — leave alone), and
`Baya.Application/Common/StaffRoles.cs` + `Features/PatientCareRecords/PatientAccess.cs:17` (the
narrow-scope pattern to mirror).
## 2. Required reading (do this first)
- [issues.md](issues.md) H-04/H-05 — evidence with file:line; don't re-audit.
- `DynamicPermissionService.cs`, `RoleNames.cs`, `RoleManagerService.cs`,
`DemoWorldDefinitions.cs`/`DemoWorldSeeder.cs`, `Features/Bookings/BookingRoles.cs` and the three
handlers listed in H-05, `Common/StaffRoles.cs`, `PatientAccess.cs`.
- `dev/post-phase/refinement/RUNBOOK.md:115-130` and
`dev/shared-working-context/reports/refinement-phase-2-report.md` — the docs that currently
overstate what works (you will correct them).
- `server/CLAUDE.md` + `server/CONVENTIONS.md` (patterns, zero-new-warnings gate).
- REQ-031 in `dev/shared-working-context/frontend/requests/for-backend.md` (the RBAC endpoints the
frontend's `/admin/roles` grid expects).
## 3. Scope — build this
1. **Make DynamicPermission passable.**
- `DynamicPermissionService.CanAccess`: treat `RoleNames.SuperAdmin` (and `RoleNames.Admin`) as a
full bypass — mirroring how the rest of the codebase treats super_admin — instead of the single
literal `"admin"`.
- **Seed RoleClaims** for the fine-grained roles (`finance`, `support`, `moderation`) mapping each
to the controller/action set it should reach (finance → payouts/refunds/invoices/BNPL admin;
support → tickets/alerts; moderation → reviews queue). Seed via the same idempotent mechanism the
demo seeder uses (or a migration `HasData` if that's the house style — check how roles themselves
are seeded). A fresh clone must have a working admin console out of the box.
2. **Expose role management over HTTP (REQ-031).** Add a `RolesController` (or `AdminRolesController`)
under `Controllers/V1` wiring the existing `Features/Role` handlers: list roles (+ their claims),
grant/revoke a user role, update role permissions. Gate it to `super_admin` only. Follow the house
controller conventions (snake_case routes, ApiResult envelope, versioning). Mark REQ-031 delivered
in `for-backend.md`.
3. **Split `BookingRoles.Admin`.** Introduce purpose-specific sets (suggested:
`BookingRoles.ClinicalAccess = [Admin, SuperAdmin]`,
`BookingRoles.Financial = [Admin, SuperAdmin, Finance]`, keep a `Staff` set where genuinely all
staff belong, e.g. read-only booking lookups for ticket context). Update the users:
`GetCareInstructionsQuery` (clinical), `TransitionBookingStatusCommand` (financial — it arms the
payout trigger), `GetNursePayableBalanceQuery` (financial), and review the remaining
`BookingRoles.Admin` consumers (`CancelSessionCommand`, `CancelBookingCommand`,
`SubmitCareInstructionsCommand`, `GetVisitVerificationQuery`, `ListBookingsQuery`,
`GetBookingDetailQuery`, `GetBookingRequestQuery`) — assign each the narrowest set that matches
what it exposes. Justify each choice in the report.
4. **Docs honesty.** Correct `RUNBOOK.md` + the refinement-phase-2 report claims about the admin
personas; document the finance/support/moderation scopes somewhere durable (server/CLAUDE.md or a
product note).
## 4. Mocks & seams in this phase
None. This is authorization wiring only — no vendor seams, no schema beyond possible RoleClaim seed
rows.
## 5. Critical rules you must not get wrong
- **Do not weaken anything:** endpoints currently gated by DynamicPermission must stay gated; you are
making the policy *satisfiable*, not optional. No `[AllowAnonymous]` anywhere in this phase.
- Tenancy behavior (404-not-403 on cross-tenant) is correct today — don't touch those handlers except
for the role-set swap.
- The role-claim seed must be **idempotent** (the seeder runs on every Dev boot) and must not touch
production posture (respect the refinement-phase-5 config guard patterns).
- Support/Moderation must lose clinical + financial access, but must NOT lose what tickets genuinely
need (booking summary context for a thread). Check the ticket flows before narrowing a shared query.
- Zero new build warnings; enum/role names are stable wire contracts — don't rename `super_admin` etc.
## 6. Definition of Done
- Integration tests (Testing env, house `WebApplicationFactory` pattern): super_admin 200s on a
representative endpoint of every admin console family; finance 200s on payouts + 403s on
verification decide; support 403s on `GET bookings/{id}/care_instructions` and on
`TransitionBookingStatus`; moderation 200s on the review queue only. Grant/revoke via the new
controller round-trips.
- `dotnet build Baya.sln` 0 new warnings · `dotnet test Baya.sln` green (all prior tests + new).
- REQ-031 marked delivered; RUNBOOK/report corrections committed.
## 7. How to test (human)
1. Boot per RUNBOOK; log in as 09120000020 (super_admin) via dev OTP.
2. Call `GET api/v1/platform_configs`, the payout batches list, the moderation queue — all 200.
3. Log in as 09120000021 (finance): payouts 200; verification decide → 403.
4. Grant `moderation` to a fresh user via the new RolesController; verify the queue opens for them.
## 8. Hand off & document
- Tick H-04/H-05 in [issues.md](issues.md) with the commit hash.
- Update `for-backend.md` (REQ-031), `server/CLAUDE.md` (authz model paragraph), RUNBOOK.
- Write `dev/shared-working-context/reports/hardening-phase-1-report.md` + a backend handoff note if
Phase 4 runs as a separate agent.
@@ -0,0 +1,114 @@
# Hardening Phase 2 — Money-path mock integrity (no fake success on real data)
> Sever the dangling cross-imports the phase-4 de-mock left behind — the refunds and BNPL mocks still
> read retired in-memory stores keyed by REAL server ids — split the seams whose real halves are
> already live (verification nurse flow, nurse earnings), and stop discarding the delivered
> `bookingId`. After this phase, nothing in the money path can show a fake success against real data.
> **Track:** frontend · **Depends on:** Phase 0 · **Unlocks:** Phase 5
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-06 … H-10** from [issues.md](issues.md). Refinement-phase-4 flipped 14 domains to real but
left 7 mocked; two of the mocked ones (`refunds`, `bnpl`) still hard-import the **mock modules** of
now-real sibling domains (`bookings`, `bookingRequests`), whose fixture stores no longer correspond to
anything — so a real customer's cancel 404s and the BNPL wizard either 404s or fabricates a client-side
"booking" while the real request expires unpaid. Meanwhile two seams hold real, shipped backend halves
hostage to a single flag (`verification` nurse flow — which silently breaks nurse discoverability
because the real search gate needs the real `is_verified`; `payouts` nurse reads — REQ-025 is live).
**What already exists (do not rebuild):** the per-domain seam pattern (`apis/index.ts` selects
mock/real), all the real `clientApi.ts` implementations named below, the b6 verification endpoints, the
REQ-025 nurse payout endpoints (`NursePayoutsController.cs:33-48`), and the REQ-017 `bookingId` on
`BookingRequestDto`.
## 2. Required reading (do this first)
- [issues.md](issues.md) H-06/H-07/H-08/H-09/H-10 — full evidence; don't re-audit.
- `client/CLAUDE.md` → "The services/{domain} reference pattern" + the de-mock status block.
- The files cited per finding (refunds/bnpl/bookings/bookingRequests mockApis + constants, checkout
pages, `PublishGate.tsx`, `MyServicesList.tsx`, payment `clientApi.ts`/`invalidations.ts`,
payouts/verification services).
- `dev/shared-working-context/reports/mocks-registry.md` — you will update the affected rows.
## 3. Scope — build this
1. **H-06 — refunds mock must see real bookings.** While `USE_REFUNDS_MOCK=true` (its REQs land in
Phase 4), rewire `services/refunds/apis/mockApi.ts` to resolve the booking through the **selected**
`bookingsApi` (the seam, real today) instead of importing `mockGetBookingForRefund` from the
bookings mock module. The mock computes the policy preview locally off the real booking's
status/sessions/amounts and simulates the cancel/refund state machine on top. A real booking id
must produce a coherent preview + cancel flow (mock-side state), never a 404.
2. **H-07 — BNPL must not fake success.** Two parts:
- Derive the checkout BNPL CTA visibility instead of the bare `BNPL_ENABLED=true`
(`services/payment/constants.ts:23`, used at `checkout/page.tsx:214`): hide the branch whenever
`USE_BNPL_MOCK` is true while `bookingRequests`/`payment` are real (mixed state = the dangerous
combination). An honest absent button beats a fabricated booking.
- For the dev/demo path that remains reachable (both mocked, e.g. Testing), rewire
`services/bnpl/apis/mockApi.ts:5-8` to go through the **selected** `bookingRequestsApi`/
`bookingsApi` seams, never the raw mock modules.
3. **H-08 — split the verification seam; make the nurse trust flow real.**
- Split `VerificationApi` selection per half: nurse-facing methods (status, start, identity,
Shahkar, bank, document upload, credentials, trust badge) go **real** now — the clientApi is
already 1:1 per the mocks registry; admin-facing methods (queue, case, decideStep, signed doc
URL) stay mocked until REQ-034 (Phase 4). Follow whatever per-method selection shape is cleanest
under the existing seam pattern (two flags, or a composed api object) and record it in the
registry.
- Wire `PublishGate.tsx` to the real status (its CTA currently calls nothing — `:59-68`), and gate
the variant-activation affordance in `MyServicesList.tsx` on real verification status with an
honest explainer, so a nurse can no longer "publish" into invisibility. Remove/dev-gate the
`__mockApproveAll` simulator button accordingly.
4. **H-09 — split the payouts seam.** Nurse reads (`earnings_balance`, `earnings`, `{id}` detail,
history) go real (`payouts/apis/clientApi.ts:163-187` already implements them); admin batch methods
stay mocked until REQ-036. Same split mechanics as verification.
5. **H-10 — stop nulling `bookingId`.** `services/payment/apis/clientApi.ts:55-75`: read `bookingId`
off the wire (drop the `Omit`/`bookingId: null` and the stale REQ-017-pending comments in
`clientApi.ts` and `types.ts:98-110`); confirm `invalidations.ts` now receives it and the
confirmation page renders the booking + invoice deep links.
## 4. Mocks & seams in this phase
Touches the `refunds`, `bnpl`, `verification`, `payouts` seams (splits/rewires; flags flipped only for
the halves whose backend is live). Update each row in
`dev/shared-working-context/reports/mocks-registry.md` — including correcting the stale "reads the
shared f7 store" descriptions.
## 5. Critical rules you must not get wrong
- **A mock may only reach sibling data through the selected seam** (`services/{domain}/apis/index.ts`),
never by importing a sibling's `mockApi.ts` — that's the root cause you're eradicating. Grep for
remaining cross-mock imports before finishing.
- Money is served IRR digit-strings, BigInt-safe — the refunds mock's local preview must reuse the
existing money utils and reconcile (refund + fee = captured), never float math.
- Verified-only search, two-stage disclosure, and "client never computes what the server owns"
invariants all still hold; the verification split must not let the client write `is_verified`.
- A 409 on the money path is benign convergence, never a toast (existing rule).
- Keep i18n complete for any new copy (both message files); `npm run check` owns the gate.
## 6. Definition of Done
- Real journey (seeded world, real server): create booking request → accept → pay → **cancel**
policy preview renders with reconciling numbers (no 404).
- BNPL CTA absent on real checkout while the domain is mocked; the D1-D5 wizard is unreachable with a
real request id (and works fully in the all-mock dev mode).
- Fresh nurse: real verification wizard drives the real b6 endpoints; variant activation is gated
until the real status approves; after approval + activation the nurse appears in real search.
- Nurse earnings screen shows real amounts from the live endpoints.
- Payment confirmation shows working "view booking" + "download invoice" deep links after a real
capture.
- `npm run check` + `npm run test:ci` green; no `services/*/apis/mockApi` imports across domain
boundaries (grep-proof in the report).
## 7. How to test (human)
1. Full customer journey per §6 against the RUNBOOK setup; screenshot the cancel preview.
2. `grep -r "apis/mockApi" client/src/services --include=*.ts | grep -v "own domain"` → empty.
3. Nurse persona 09120000003 (unverified): services page shows the gated state; run the real
verification flow; confirm search visibility flips after approval.
## 8. Hand off & document
- Tick H-06…H-10 in [issues.md](issues.md) with commit hashes.
- Update `client/CLAUDE.md` de-mock status block + `mocks-registry.md` rows.
- Write `dev/shared-working-context/reports/hardening-phase-2-report.md`; file new REQs if a split
exposed a missing server read (append to `for-backend.md`, Phase 4 picks them up).
@@ -0,0 +1,99 @@
# Hardening Phase 3 — Session & error-surface hardening
> Close the shared-device data leak (logout leaves every domain's cache warm for the next login), turn
> the raw stack-trace error boundary into a branded recovery surface with route-level error pages, and
> localize the four hardcoded-English failure toasts. Plus: client-side capability gating for the four
> admin consoles that render without a check.
> **Track:** frontend · **Depends on:** Phase 0 · **Unlocks:** — (independent polish, safe anytime after 0)
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-11 … H-14** from [issues.md](issues.md). All four are verified, none is touched by any other
phase, and none needs backend work.
**What already exists (do not rebuild):** `useLogout` as the single logout path, the
`AuthAccountError` recovery-card pattern (mirror it for the boundary), `dispatchToast`/`ToastBridge`
(the non-React toast bridge — keep it, localize what flows through it), `useAdminCapabilities()`
(`src/hooks/capabilities.ts`) and the non-leaking access-denied patterns used elsewhere in the admin
screens.
## 2. Required reading (do this first)
- [issues.md](issues.md) H-11/H-12/H-13/H-14 — evidence; don't re-audit.
- `client/src/services/auth/hooks/{useLogout,useVerifyOtp,useSelectRole}.ts`,
`client/src/lib/query/{queryClient.ts,QueryProvider.tsx}`,
`client/src/components/common/ErrorBoundary.tsx` + its two consumers,
`client/src/lib/api/client.ts` + `client/src/lib/toast/*`,
the four admin pages (`admin/{audit,verification,tickets,roles}/page.tsx`) + `AdminLayout.tsx`.
- `client/CLAUDE.md` → error contract for `clientFetch`, i18n rules, unit-testing rule.
- Next.js `error.tsx` / `global-error.tsx` file conventions.
## 3. Scope — build this
1. **H-11 — cache isolation across sessions.** `useLogout.onSettled`: `queryClient.clear()` (replace
the auth-only `removeQueries`). Symmetrically clear/reset before seeding the new session in
`useVerifyOtp.onSuccess` and `useSelectRole.onSuccess`, so User B never sees User A's cached
patients/bookings/addresses/tickets/notifications on a shared device. Check nothing depends on
surviving cache entries across logout (geo/catalog reference data may simply refetch — correct).
2. **H-12 — real error surfaces.**
- Rebuild `ErrorBoundary`'s fallback as a branded, i18n'd recovery card (mirror
`AuthAccountError`): translated generic message + retry (re-render children / reload), raw
`error.toString()` + component stack rendered **only** in development. Keep the class component;
add the error-reporting hook point (a single function you can later wire to a service — no TODO
comment left behind).
- Add `src/app/[locale]/error.tsx` and `global-error.tsx` so errors thrown above the shells
(RoleGuard, providers, layouts) get the same branded treatment instead of Next's default. Respect
the root-layout constraint (`global-error` must render its own `<html>`; keep locale/dir sane).
3. **H-13 — localize the failure toasts.** `lib/api/client.ts:57,80,86,91`: replace the four English
literals with a small locale-keyed dictionary module (the fetch layer already knows the locale;
it can't call `useTranslations`). Add keys (suggest an `errors` namespace) to **both**
`messages/en.json` and `messages/fa.json`. Keep the error contract exactly (which statuses toast,
which throw) — only the copy source changes.
4. **H-14 — capability-gate the four admin consoles.** Add a shared `CapabilityGuard`
(`need: keyof AdminCapabilities`) that early-returns the existing non-leaking access-denied state,
and wrap the page bodies of `admin/audit` (canViewAudit), `admin/verification` (canVerify),
`admin/tickets` (canManageTickets), `admin/roles` (canManageRoles) — before their data hooks fire.
Display convenience, not security (the server enforces after Phase 1) — but a scoped admin
deep-linking must see the denied card, not the data.
## 4. Mocks & seams in this phase
None.
## 5. Critical rules you must not get wrong
- `queryClient.clear()` on logout must run **after** the server revoke call is issued (keep the
existing revoke → clear-tokens → dispatch → redirect ordering; the cache clear joins it, doesn't
reorder it).
- Don't toast inside hooks for 401/403/5xx (already toasted by `clientFetch`) — unchanged rule.
- `global-error.tsx` replaces the root layout when it triggers — it must be self-contained (no
providers assumed), bilingual-safe, and never crash itself.
- Every new user-visible string in both message files; shared components (`CapabilityGuard`, the new
boundary fallback if extracted) get co-located tests per the client testing rule.
- Do not turn the boundary into a swallow-everything — rethrow/log in dev so DX doesn't regress.
## 6. Definition of Done
- Shared-device test: login A (customer with patients) → logout → login B → B never sees A's data
(verify React Query devtools cache is empty right after login).
- Throw a test error inside a page in dev → branded Persian card with retry; stack visible in dev
build only. Route-level error files exist and render.
- On `fa`, kill the API and trigger a fetch → Persian network-error toast; expire the session → Persian
session-expired toast + login redirect (Phase 0 behavior).
- Finance-scoped admin deep-links `/admin/audit` → access-denied card, no data fetch fired.
- `npm run check` + `npm run test:ci` green (new tests for CapabilityGuard + boundary fallback).
## 7. How to test (human)
1. Two seeded accounts on one browser: full logout/login swap watching the patients screen.
2. Temporarily `throw new Error('boom')` in a page render; check both dev and `npm run build && start`.
3. Stop the API mid-session on `/fa`; watch toast language; restart, expire tokens, watch the redirect.
4. Login as 09120000021 (finance), deep-link the four consoles.
## 8. Hand off & document
- Tick H-11…H-14 in [issues.md](issues.md) with commit hashes.
- Update `client/CLAUDE.md` (error contract copy source, error.tsx files in the structure tree,
logout lifecycle).
- Write `dev/shared-working-context/reports/hardening-phase-3-report.md`.
@@ -0,0 +1,107 @@
# Hardening Phase 4 — Contract completion batch (close the Tier B/C REQs)
> Deliver the remaining open contract requests so the last seven mocked client domains have a real
> backend to flip to: customer refunds, BNPL reads, review eligibility, family-record access, admin
> console deltas, the admin halves of verification/refunds/payouts, and the partner portal's identity
> signal + self-scoped reads. The sequel to refinement-phase-3 (which delivered Tier A).
> **Track:** backend · **Depends on:** Phase 1 (RBAC — the admin endpoints here must be gated by the
> now-working policy) · **Unlocks:** Phase 5
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-15, H-16a** from [issues.md](issues.md). The canonical spec for every item is the REQ entry
in [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) plus the Tier B/C
sections of [refinement-phase-3](../refinement/refinement-phase-3-contract-batch.md) — this file
sequences and annotates; it does not restate shapes. **Re-check each REQ's `Status:` line before
building — some were partially delivered** (REQ-025 nurse reads are live; REQ-027's GET/PUT
care_record exists; REQ-017/my_review landed earlier; Phase 1 may have delivered REQ-031; Phase 2 may
have appended new REQs — pick those up too).
**What already exists (do not rebuild):** the b11 refund engine (fee-leg decomposition, clawback
fork), the b12 BNPL machine, the b14 review/moderation + care-record entities, the b13 payout engine +
`nurse_payouts` reads, the b6 verification pipeline + signed-URL storage seam, partner centers with
kebab-case admin routes, and the refinement-phase-3 house pattern for contract deltas
(migration + swagger regen + tests per REQ).
## 2. Required reading (do this first)
- `for-backend.md` — every REQ still `open`; the proposed shapes there are the contract.
- [refinement-phase-3-contract-batch.md](../refinement/refinement-phase-3-contract-batch.md) §Tier B/C
+ its "how to test" — mirror its delivery pattern.
- `server/CLAUDE.md`, `server/CONVENTIONS.md`; the relevant `product/` docs per area (refund policy,
BNPL, verification, partners) — business rules are decisions, not guesses.
- Phase 2's report + any REQs it filed.
## 3. Scope — build this (one commit-sized slice per bullet, in this order)
1. **REQ-019/020/021 — customer refunds** (unblocks `refunds`): `POST bookings/{id}/cancel`
(customer-initiated, tenancy-checked), `GET bookings/{id}/cancellation_policy` preview — and
**define the canonical `cancellation_policy_code` set** (the client invented
`free_24h`/`partial_under_24h`/`customer_no_show`; decide with `product/` and publish the enum),
`GET refunds/by_booking/{id}` with fee-leg decomposition. Reuse the b11 engine; a customer cancel
is never a self-issued refund — it routes through the existing policy/approval model.
2. **REQ-022/023/024 — BNPL reads** (unblocks `bnpl`): `checkout_bnpl/options/{requestId}`,
`checkout_bnpl/schedule`, eligibility accepting the D3 KYC fields, wallet installments
(provider-reported — D5 is NOT a Balinyaar ledger), customer-facing `bookingId` on settle.
3. **REQ-026 — reviews**: `bookings/{id}/review_eligibility` + confirm/finish `my_review` (verify
what already shipped) + the masked-author decision.
4. **REQ-027 leftovers — family care record**: the `record_access` check endpoint; confirm the
product decision recorded for the family-owned record (the GET/PUT already exist with `long` ids —
the id-type reconciliation is client-side, Phase 5). Structured `taskResults` on visit notes if the
REQ still asks.
5. **REQ-029/030 — admin console deltas**: config `updatedAt`/`updatedBy`; audit filters
(`actor_id`/`action`/`from`/`to`). (REQ-031 should be done by Phase 1 — verify, don't duplicate.)
6. **REQ-034/035/036 — the admin halves** (unblock `verification`/`refunds`/`payouts` admin UIs):
verification queue + case + per-step decide + on-demand signed doc URL; refund preview/initiate/
approve/reject (ticket-linked, reusing b11); payout batch preview/run (idempotency-keyed)/retry/
record-transfer-reference (reusing b13). All gated with the Phase-1 policy + fine-grained roles
(finance vs moderation vs support scopes as seeded).
7. **REQ-032/033/038 — partner portal**: `administersPartnerCenterId` on `MeResult` + a seeded demo
partner admin; `centers/me`, `centers/me/nurses`, `centers/me/bookings`, `centers/me/settlement`
(identity-derived tenancy — resolve the center from the caller, never from a client-sent id);
invoice `totalIrr`; activate/suspend toggle if still open.
## 4. Mocks & seams in this phase
No new vendor seams. Everything reuses existing engines behind existing seams. Update
`mocks-registry.md` rows only where a "Make it real →" step is now shorter.
## 5. Critical rules you must not get wrong
- **Additive only** — no breaking DTO/route changes; the 14 already-real client domains must keep
working untouched.
- Money invariants: refund Σ ≤ captured (409 otherwise), fee-leg decomposition, pre/post-payout fork
via `INursePayoutStatus`, BNPL net-of-fee model, payout invariant-to-method. All exist — wire, don't
re-derive.
- Tenancy on every new customer/nurse/partner read (owner-or-staff, 404-not-403); partner reads are
identity-derived (`/me`-style), never id-parameterized.
- Admin endpoints: `[Authorize(ConstantPolicies.DynamicPermission)]` + correct fine-grained scope —
Phase 1's tests must still pass.
- Signed URLs short-lived, fetched on demand (the b6/f15 contract). Sequential invoice numbering and
Moadian states untouched.
- Per-REQ: migration if schema changes, swagger regenerated into `dev/contracts/openapi/`, tests per
the refinement-3 pattern, `Status:` updated in `for-backend.md` in the same change.
## 6. Definition of Done
- Every REQ listed above is either delivered (status updated, tested) or explicitly re-deferred with
a reason recorded in `for-backend.md` — no silent skips.
- `dotnet build Baya.sln` 0 new warnings · `dotnet test Baya.sln` green.
- Swagger regenerated; the refinement-3 §7 smoke calls for Tier B/C pass against the seeded world
(customer cancel creates a refund; `centers/me` resolves the caller's center; admin
queues/previews return their shapes under the right roles).
## 7. How to test (human)
Per tier, via Swagger against the demo world: cancel a paid booking as its customer (policy preview →
cancel → refund visible via by_booking); fetch BNPL options for an accepted request; open the
verification queue as super_admin and decide a step; preview + run a payout batch as finance; log in
as the seeded partner admin → `centers/me` chain returns their center only.
## 8. Hand off & document
- Tick H-15/H-16a in [issues.md](issues.md) (commit hashes per REQ group is fine).
- Update `for-backend.md` statuses, `mocks-registry.md`, `server/CLAUDE.md` if the project map moved.
- Write `dev/shared-working-context/reports/hardening-phase-4-report.md` + a handoff note listing
exactly which flags Phase 5 may now flip.
@@ -0,0 +1,96 @@
# Hardening Phase 5 — Final de-mock & partner reachability (zero mocks left)
> Flip the last mocked client domains to the Phase-4 backend, reconcile the patientRecords id-type
> mismatch that makes its PUT write-unsafe, and make the partner portal reachable from login and
> actually gated. Exit criteria for the whole chain: **no `USE_*_MOCK = true` remains anywhere.**
> **Track:** frontend · **Depends on:** Phases 2 & 4 · **Unlocks:** — (chain complete)
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
Fixes **H-16b, H-17** and retires the remaining flags from [issues.md](issues.md). After Phase 2 the
mocked surface is: `refunds`, `bnpl`, `admin`, `partnerCenter`, `patientRecords`, plus the admin
halves of `verification`/`payouts`. Phase 4 delivered (or explicitly re-deferred) their REQs — its
handoff note says exactly which flags may flip. This mirrors refinement-phase-4: **a flip is never
assumed to be a pure flag flip** — check each `clientApi.ts` mapper against the delivered DTOs first
(that phase's lesson: delivered fields were being null-overridden by stale mappers).
**What already exists (do not rebuild):** every domain's real `clientApi.ts`, the seam-split
mechanics from Phase 2, the partner portal screens, `useMyPartnerCenter`'s access-denied pattern, the
Phase-0 auth gate.
## 2. Required reading (do this first)
- Phase 4's report + handoff note (which REQs landed; which were re-deferred — a re-deferred REQ means
its domain **stays mocked** and you record why, you don't force it).
- [issues.md](issues.md) H-16b/H-17; the refinement-phase-4 file + report (the mapper-fix lesson).
- `client/CLAUDE.md` de-mock status block; `mocks-registry.md` frontend section.
- The regenerated swagger in `dev/contracts/openapi/` for every DTO you map.
## 3. Scope — build this
1. **H-17 — patientRecords id reconciliation, then flip.** Client `Medication`/`RoutineItem`/
`CareTask.id` are `string` ('m1'/'r1'/'t1' seeds) vs the wire's `long`
(`CarePlanDtos.cs:11-15`). Reconcile before flipping: existing items carry the server's numeric id
(stringified is fine if the type stays string — but the PUT must send what the server accepts);
**new items omit the id** and let the server assign. Update the mapper + `useUpdateCareRecord`'s
optimistic path; then `USE_PATIENT_RECORDS_MOCK=false`. The nurse visit-note half is already
contract-real — don't disturb it.
2. **H-16b — partner reachability + gate.**
- `Me` gains `administersPartnerCenterId` (REQ-038); `resolveRoleDestination` gains the partner
branch (a partner admin lands on `/partner` after login; add the routing unit test).
- Gate the portal at the **layout** level: a `PartnerAccessGuard` in `partner/layout.tsx` resolves
`useMyPartnerCenter()` once and renders the non-leaking access-denied state on 403/404 before any
child page mounts; `nurses`/`bookings`/`settlement` pages stop fetching before the gate resolves.
- Flip `USE_PARTNER_MOCK=false` onto the `centers/me*` reads (REQ-032/033).
3. **Flip the rest, mapper-checked, in dependency order:** `refunds` (REQ-019/020/021 — retire the
Phase-2 mock-side preview), `bnpl` (REQ-022/023/024 — restore the checkout CTA derivation to
enabled-when-real; delete the dev gateway-harness page if the real provider redirect replaces it),
`verification` admin half (REQ-034 — admin queue/case/decide/signed-URL onto real), `payouts` admin
half (REQ-036 — batch preview/run/retry/reference onto real), `admin` (REQ-029/030/031 — config/
audit/alerts/holidays/RBAC grid real; the RBAC grid consumes Phase 1's RolesController).
4. **Cleanup + honesty pass.** Delete now-unused mockApi modules and cross-mock helpers
(`mockGetBookingForRefund`, `mockInsertConvertedBooking`, `__mockApproveAll`, …) unless a Testing
path genuinely uses them (then say so in the registry). No dead code. Update the
`client/CLAUDE.md` de-mock block (should read "22/22 real"), `mocks-registry.md` (frontend rows →
🟢), and the project-structure tree if files moved/died.
## 4. Mocks & seams in this phase
This phase **retires** the frontend mock seams. The seam pattern itself stays (it's the test/dev
affordance); only the defaults flip. Any domain that must stay mocked (re-deferred REQ) keeps a
registry row with the reason + pull-trigger.
## 5. Critical rules you must not get wrong
- **Mapper-check every flip** against the real swagger — the refinement-4 lesson. A delivered field a
stale mapper drops is a silent regression.
- Money stays served IRR digit-strings; D5 wallet is provider-reported, never a Balinyaar ledger; the
BNPL confirmation reuses the card confirmation (`?method=bnpl`).
- Partner tenancy is server truth — the client guard is chrome; never pass a center id from the
client to the `me`-scoped routes.
- Published-only reviews, two-stage disclosure, nurse append-only records — unchanged invariants.
- Per flip: run the affected screen against the live server before moving to the next flag.
## 6. Definition of Done
- `grep -rn "USE_.*_MOCK = true" client/src/services` → empty (or each survivor justified in the
registry + issues.md).
- Journey A end-to-end on real data: login → search → request → accept → pay (card AND BNPL) →
booking → cancel/refund path visible → review. Journey B: verification → publish → inbox → accept →
EVV → earnings. Admin: verification decide, refund approve, payout run. Partner: login →
auto-routed → own center only.
- `npm run check` + `npm run test:ci` green; no dead mock code.
## 7. How to test (human)
Run the two journeys + the admin/partner spot-checks above against the RUNBOOK setup. The demo world
personas: customers 0912000000x, nurses (incl. unverified 09120000003), admins 0912000002x, plus the
Phase-4 partner admin seed.
## 8. Hand off & document
- Tick H-16b/H-17 (and the flip checklist) in [issues.md](issues.md) with commit hashes.
- Final updates: `client/CLAUDE.md`, `mocks-registry.md`, `for-backend.md` (all REQs terminal).
- Write `dev/shared-working-context/reports/hardening-phase-5-report.md` declaring the chain complete
(or listing exactly what remains and why).
+163
View File
@@ -0,0 +1,163 @@
# Hardening issue ledger (verified findings)
Every item below was found by one audit agent and **re-confirmed by an independent adversarial
verifier** reading the code (and, for H-01, probing the running dev server). Evidence is file:line at
the time of the audit (2026-07-16) — re-locate if lines drifted, but do not re-litigate the finding.
**This file is the loop's progress state.** When a phase fixes an item, tick it `[x]` and append
`— fixed in <commit>` on the same line. `LOOP-PROMPT.md` reads this file to decide what's left.
---
## Phase 0 — auth gate & session liveness
- [ ] **H-01 (blocker) — the auth gate never executes at runtime.**
`client/middleware.ts` is correct as written (PUBLIC_PATHS = `['/login']`, redirect on dead token)
but never runs: live cookie-less `GET /fa/admin` → 200 full HTML, bare `GET /` → 404 (no next-intl
locale redirect either) — reproduced under Turbopack **and** `--webpack`. Cause: Next.js infers the
workspace root from a stray `C:\Users\Lenovo\pnpm-lock.yaml` (startup warning names it), so the
middleware file is never bound. Also: Next 16 deprecates `middleware.ts` for `proxy.ts`, and the
docs place the file "at the same level as `pages` or `app`" — for this repo that's `client/src/`.
Fix: pin `turbopack.root` (+ `outputFileTracingRoot`) in `client/next.config.mjs`; migrate the file
to `src/proxy.ts`; **prove the gate runs with a runtime probe** (curl DoD).
- [ ] **H-02 (blocker) — `isTokenAlive` can never read the real token (JWE).**
Server: `server/src/Infrastructure/Baya.Infrastructure.Identity/Jwt/JwtService.cs:116-128`
`EncryptingCredentials(Aes128KW, Aes128CbcHmacSha256)` ⇒ the access token is a 5-segment JWE.
Client: `client/src/lib/auth/token.ts:24-38` JSON-parses segment 1 ⇒ always `null`
`isTokenAlive` always `false` for real tokens. Consequences: (a) once H-01 is fixed the middleware
would redirect-loop **logged-in** users; (b) `getServerAuthState` (`client/src/lib/auth/server.ts`)
seeds `isAuthenticated=false` on every hard reload. The wire already returns
`accessExpiresAt`/`refreshExpiresAt` (`client/src/services/auth/types.ts:62-66`): persist a
readable companion expiry cookie in `persistAuthTokens` and check that instead of decoding.
- [ ] **H-03 (high) — anonymous visitor to a private shell = infinite splash, never a login ask.**
`useMe()` is enabled only when `isAuthenticated`; for a cookie-less session that's permanently
`false`, so `useRoleHydration()` never leaves `loading` and `RoleGuard`
(`client/src/components/auth/RoleGuard.tsx:55`) renders `AuthSplash` forever (splash markup
confirmed in served HTML). Add an explicit `unauthenticated` state → redirect to `/login?next=…`
with return-URL honored post-login.
## Phase 1 — admin RBAC & staff role scopes (server)
- [ ] **H-04 (blocker) — DynamicPermission RBAC is structurally dead; the whole admin API 403s.**
`DynamicPermissionService.cs:9` passes only the literal role `"admin"`; the seeded personas hold
`super_admin`/`finance` (`DemoWorldDefinitions.cs:144-145`, distinct constants `RoleNames.cs:13,17`).
No RoleClaim is ever seeded, and the only claim writer
(`RoleManagerService.ChangeRolePermissionsAsync`, `RoleManagerService.cs:145-208`) has **no HTTP
surface** (the `Features/{Role,Admin}` handlers exist but no controller exposes them). Every
`[Authorize(ConstantPolicies.DynamicPermission)]` endpoint (all Admin* controllers, PlatformConfig,
Holidays, Audit, SupportAlerts, Reviews-moderate) 403s for both seeded admins. RUNBOOK.md:115-130 and
refinement-phase-2-report falsely document these personas as working — fix the docs too.
- [ ] **H-05 (high) — `BookingRoles.Admin` over-grants: Support/Moderation get clinical + financial access.**
`Features/Bookings/BookingRoles.cs:8-9` bundles Admin/SuperAdmin/Support/Finance/Moderation; used by
`GetCareInstructionsQuery.Handler.cs:31,37` (encrypted clinical notes),
`TransitionBookingStatusCommand.Handler.cs:24-25,51-54` (force `Completed` ⇒ arms the payout
dispute-window), `GetNursePayableBalanceQuery.Handler.cs:22-24` (contradicts its own comment).
Controllers carry only blanket `[Authorize]` — the handler set IS the boundary. The codebase's own
convention is narrower (`PatientAccess.cs:17` = [Admin, SuperAdmin]; `StaffRoles.cs:5-13`
"narrowest fitting scope"). Split into `ClinicalAccess` / `Financial` sets and update the handlers.
## Phase 2 — money-path mock integrity (client)
- [ ] **H-06 (blocker) — refunds mock reads the retired bookings-mock store → real cancel 404s.**
`services/refunds/apis/mockApi.ts:3,88` imports `mockGetBookingForRefund` from
`services/bookings/apis/mockApi.ts` whose fixture array (ids ~5001-5005, `:340-345` throws 404
otherwise) is orphaned since `USE_BOOKINGS_MOCK=false`. Cancel CTA renders for every real booking
(`bookings/[id]/page.tsx:79-104`) then the preview errors (`bookings/[id]/cancel/page.tsx:50-78`).
- [ ] **H-07 (blocker) — BNPL wizard runs on a disconnected mock store keyed by the real request id.**
`checkout/page.tsx:213-224` pushes the **real** `requestId` (payment+bookingRequests are real) into
the BNPL branch; `services/bnpl/apis/mockApi.ts:5-8` hard-imports `bookingRequestsMockApi` +
`mockInsertConvertedBooking` (raw mock modules, bypassing both domains' seams); store holds only
fixture ids 1-2 → 404, or on id collision `settle()` (`:260-317`) fabricates a client-side "booking"
while the real request expires unpaid. `BNPL_ENABLED=true` (`services/payment/constants.ts:23`) has
no gating. Stopgap: hide/derive the CTA while `USE_BNPL_MOCK && !USE_BOOKING_REQUESTS_MOCK`; any
surviving mock must resolve requests through the real `bookingRequestsApi` selector.
- [ ] **H-08 (blocker) — verification is 100% mocked while catalog/search are real → invisible nurses.**
`services/verification/constants.ts:8-9` `USE_VERIFICATION_MOCK=true` gates the WHOLE
`VerificationApi` (nurse flow + TrustBadge + admin queue, one interface — `types.ts:255-265`).
The mock's `__mockApproveAll` never flips the real `nurse_profiles.is_verified`
(`NurseProfile.cs:44-53`, private setter, only the real b6 `VerificationAggregator` calls it), but
variant activation is real and unconditional (`SetVariantActiveCommand.Handler.cs:15-36`), and the
search gate requires `IsVerified` (`SearchIndexMaintainer.cs:34,65,94,152`). `PublishGate.tsx:59-68`
CTA makes no API call at all. Fix: split the seam (nurse half real now — clientApi is 1:1 per the
registry; admin half stays mocked until REQ-034) and wire/gate the publish UI honestly.
- [ ] **H-09 (high) — nurse earnings fabricated although its endpoints are live.**
`services/payouts/constants.ts:13` `USE_PAYOUTS_MOCK=true`, but REQ-025 was **delivered**
`NursePayoutsController.cs:33-48` serves earnings_balance/earnings/{id}, and
`payouts/apis/clientApi.ts:163-187` already implements them. The flag is held hostage by the admin
batch methods (REQ-036, still open). Split the seam per-method (nurse reads real, admin mock).
- [ ] **H-10 (medium) — payment outcome discards the delivered `bookingId`.**
`services/payment/apis/clientApi.ts:55-75` types the response as `Omit<…,'bookingId'>` and sets
`bookingId: null` (line 72) although `BookingRequestDto.cs:46-49` carries it (REQ-017 delivered).
Kills the confirmation deep links (`checkout/return/page.tsx:84`,
`confirmation/page.tsx:94-110`) and the targeted cache invalidation (`invalidations.ts:17-24`).
Read it off the wire; drop the stale REQ-017-pending comments.
## Phase 3 — session & error-surface hardening (client)
- [ ] **H-11 (high) — logout/login never clear the query cache → cross-account data leak.**
`useLogout.ts:22-30` removes only `authKeys.all` (`['auth']`); `useVerifyOtp.ts:25-32` /
`useSelectRole.ts:19-38` touch only auth keys; `queryClient.ts:17-27` is a tab-lifetime singleton;
domain keys carry no user id. User B logging in after User A on the same device is served A's cached
patients/bookings/addresses/tickets until staleTime lapses. Fix: `queryClient.clear()` on logout +
reset on login.
- [ ] **H-12 (high) — raw stack-trace error boundary; no route-level error pages.**
`components/common/ErrorBoundary.tsx:35-58` renders hardcoded-English `error.toString()` + full
component stack, no retry, TODO-only reporting; used at `CustomerLayout.tsx:78` and
`TopBarAndSideBarLayout.tsx:104` — and sits BELOW RoleGuard/providers, while **no `error.tsx` or
`global-error.tsx` exists anywhere** under `src/app`. Replace with a branded i18n recovery card
(mirror `AuthAccountError`), dev-only stack, and add `[locale]` route-level error files.
- [ ] **H-13 (high) — 401/403/5xx/network toasts are hardcoded English.**
`lib/api/client.ts:57,80,86,91` — four `dispatchToast('…English…')` literals pass through
`dispatchToast.ts`/`ToastBridge.tsx` verbatim; no keys exist in either messages file. Violates the
project's golden rule #3 on the fa-default app. Localize via a locale-keyed dictionary (non-React
call site — the fetch layer already computes the locale).
- [ ] **H-14 (medium) — admin read-only consoles unguarded client-side.**
`admin/audit/page.tsx:25`, `admin/verification/page.tsx:50`, `admin/tickets/page.tsx:43`,
`admin/roles/page.tsx:50,121` fetch/render with no `useAdminCapabilities()` check — only the
sidebar (`AdminLayout.tsx:24-33`) hides them. (Server enforcement exists but is the dead
DynamicPermission policy — H-04.) Add a `CapabilityGuard` early-return with the non-leaking
access-denied pattern.
## Phase 4 — contract completion batch (server; Tier B/C REQs)
- [ ] **H-15 (medium) — the deferred REQ set, per the ledger** (`for-backend.md` — re-check each
status before building; REQ-031's HTTP surface may already exist after Phase 1):
REQ-019/020/021 customer refunds (cancel command, policy preview + canonical
`cancellation_policy_code` set, refund-by-booking + decomposition) · REQ-022/023/024 BNPL
(options/schedule/wallet-installments, eligibility KYC fields) · REQ-026 reviews
(eligibility/my-review reads — verify what phase 3/4 already delivered) · REQ-027 leftovers
(`record_access` check; the GET/PUT care_record endpoints already exist —
`PatientCareRecordsController.cs:44-54`) · REQ-029/030 admin config `updatedAt`/`updatedBy` + audit
filters · REQ-034 admin verification queue/signed-doc-URL/approve · REQ-035 admin refund
preview/initiate/approve/reject · REQ-036 admin payout preview/run/retry/record-reference.
- [ ] **H-16a (medium, server half) — partner portal has no identity signal or self-scoped reads.**
REQ-038: `administersPartnerCenterId` on `MeResult` + a seeded demo partner admin; REQ-032/033:
`centers/me[/nurses|/bookings|/settlement]` split reads + invoice `totalIrr`. `Me` interface
(`services/auth/types.ts:79-91`) has no partner field; `resolveRoleDestination`
(`services/auth/routing.ts:32-47`) has no partner branch.
## Phase 5 — final de-mock & partner reachability (client)
- [ ] **H-16b (medium, client half) — partner routing + portal gate + flip `USE_PARTNER_MOCK`.**
Add the partner branch to `resolveRoleDestination` off the new `/me` signal; gate the portal at the
layout level (today `partner/layout.tsx:15` RoleGuard has no `expected`, the mock resolves a
hardcoded center for ANY caller — `partnerCenter/apis/mockApi.ts:237-243` — and `nurses`/`bookings`
pages don't even await `useMyPartnerCenter`).
- [ ] **H-17 (medium) — patientRecords id-type mismatch; flip the remaining flags.**
Client `Medication/RoutineItem/CareTask.id: string` (`patientRecords/types.ts:36,45,53`, mock seeds
'm1'/'r1'/'t1') vs wire `long Id` (`CarePlanDtos.cs:11-15`) — a naive flip 400s every family-record
save; new items should omit id (server assigns). Then flip every remaining `USE_*_MOCK` (refunds,
bnpl, payouts-admin, admin, partnerCenter, verification-admin, patientRecords) as its REQs land;
delete the orphaned cross-mock helpers; update `mocks-registry.md` + `client/CLAUDE.md`.