cleanup phases 6
This commit is contained in:
@@ -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 1–5). 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.
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user