cleanup phase 5

This commit is contained in:
hamid
2026-08-02 18:33:43 +03:30
parent cd8144e653
commit e2db97392a
9 changed files with 818 additions and 21 deletions
+1 -1
View File
@@ -157,6 +157,6 @@ stays small.
| 2 Integration & dependency | **done** | 2026-07-30 | 27 files under `docs/integration/`. All 186 operations assigned to exactly one of 22 domain files (1:1 with `client/src/services/`). **Main finding: 24 phantom endpoints** — the client↔swagger axis nobody had checked; 2 are live 404s. Also 5 mis-filed REQs, 1 never-filed REQ (**C-15**), 8 client drifts, 5 config gaps. Resolved C-2, C-6, C-7, C-8, C-9 + C-3's contract half | | 2 Integration & dependency | **done** | 2026-07-30 | 27 files under `docs/integration/`. All 186 operations assigned to exactly one of 22 domain files (1:1 with `client/src/services/`). **Main finding: 24 phantom endpoints** — the client↔swagger axis nobody had checked; 2 are live 404s. Also 5 mis-filed REQs, 1 never-filed REQ (**C-15**), 8 client drifts, 5 config gaps. Resolved C-2, C-6, C-7, C-8, C-9 + C-3's contract half |
| 3 Flow atlas | **done** | 2026-08-02 | `docs/flows/` — 25 files, 23 flows, 283 gaps. 1 built · 15 partial · 7 mocked. Six findings, led by dead admin RBAC and 34 phantom client seam operations | | 3 Flow atlas | **done** | 2026-08-02 | `docs/flows/` — 25 files, 23 flows, 283 gaps. 1 built · 15 partial · 7 mocked. Six findings, led by dead admin RBAC and 34 phantom client seam operations |
| 4 Backlog reconciliation | **done** | 2026-08-02 | `docs/status/` — 262 open `BL-###` (18 blocker · 86 major · 115 minor · 43 deferred), 88 closed, from ~700 raw rows across 10 sources. Admin RBAC (BL-001/002) touches 11 of 14 business areas | | 4 Backlog reconciliation | **done** | 2026-08-02 | `docs/status/` — 262 open `BL-###` (18 blocker · 86 major · 115 minor · 43 deferred), 88 closed, from ~700 raw rows across 10 sources. Admin RBAC (BL-001/002) touches 11 of 14 business areas |
| 5 Roadmap | not started | | | | 5 Roadmap | **done** | 2026-08-02 | `docs/roadmap/` — 51 deferred items (43 + 8 filed elsewhere by severity) recorded with triggers, all 8 unbuilt tables named; `pre-launch.md` covers credential rotation (+ the FieldEncryption migration spelled out), 11 mocked rails, and the Development-in-production trade; `next-up.md` proposes 5 sequenced units; one stray finding (BL-245) verified and corrected in `backlog.md`/`decisions.md` along the way |
| 6 Archive & prune | not started | | | | 6 Archive & prune | not started | | |
| 7 Skills & guardrails | not started | | | | 7 Skills & guardrails | not started | | |
+62 -1
View File
@@ -118,4 +118,65 @@ that piece and not another.
## Handoff ## Handoff
_(filled in by the agent that runs this phase)_ **Done 2026-08-02 against `cd8144e`, in one session, single agent (no subagent fan-out needed — the inputs
were five already-triaged status files plus a handful of small product docs, not a fresh harvest).**
### What was produced
`docs/roadmap/``pre-launch.md`, `deferred.md`, `tech-debt.md`, `next-up.md`, `index.md`. All five
verification checklist items pass: every deferred item in `backlog.md` (51 total — the labeled 43 plus 8
more filed by severity elsewhere that also carry deferred status) appears in `deferred.md` with its trigger
preserved in substance; all 8 unbuilt product tables are named individually; `pre-launch.md` covers credential
rotation, all 11 still-mocked production rails, and the Development-in-production trade; every `next-up.md`
unit names the backlog ids it closes and the flows it affects; proposal is labeled apart from record
throughout (index.md marks each claim **recorded** or **this phase's proposal** explicitly).
### How it was verified
Read `backlog.md`, `implemented.md`, `decisions.md`, and `backlog-closed.md` in full (not sampled) since
`next-up.md`/`deferred.md`/`pre-launch.md` all need to cite exact `BL-###` ids. Cross-checked the phase
brief's tech-debt candidates against the actual code rather than restating them: **two turned out overstated.**
"Absence of E2E tests over the money paths" — false as stated; `Baya.Test.Api` and `Baya.Test.Foundation`
carry ~119 test files with real WebApplicationFactory integration coverage over Payments/Bookings/BNPL/
Payouts/Refunds. The real gap, confirmed by grepping for Playwright/Cypress in `client/package.json` (none),
is that nothing automated drives a real browser against a real API — a narrower, more precise claim than the
brief's. "Test coverage asymmetry" — also not a raw-count gap (119 server files vs 125 client files); the
actual asymmetry is that `docs/rules/server/cqrs.md` makes testing mandatory for every new feature while root
`CLAUDE.md`'s client gate is reactive ("if you touched a tested component"). Both corrected in
[tech-debt.md](../roadmap/tech-debt.md) rather than carried forward as-stated. One new debt item was found
and added that the brief didn't name: no OpenAPI→TypeScript codegen pipeline exists (confirmed absent by grep),
which is the mechanism that would have caught the client/server DTO drifts phases 2 and 3 had to discover by
hand.
One deferred item, [BL-245](../status/backlog.md#deferred-43), carried its own trigger — "phase 5 verification
pass" — naming this phase directly. Rather than deferring it again, it was executed: grepped the server for
`SuspendNurse`/`ResolveSupportAlert`/`FlagConcern`. Two of three are real and code-traced
(`ResolveSupportAlert` + `AssignSupportAlert` in `SupportAlertsController.cs`; nurse suspension as
`AdminSuspendVerificationCommand`); only `FlagConcern` is genuinely absent. Corrected in place in `backlog.md`
and logged in `decisions.md`, both outside this phase's own output folder — a deliberate, narrow exception to
"stay in your own phase's files," made because the alternative was knowingly publishing a roadmap that cites
a stale row its own source phase had flagged as needing exactly this check.
### The sequencing call worth flagging
Applying the brief's stated principle (money/trust lies > unusable > mocked→real > new surface) literally
puts admin RBAC ([BL-001](../status/backlog.md#blockers-18)/[BL-002](../status/backlog.md#blockers-18)) third
in `next-up.md`, behind two smaller truth-fixes (nurse-verification status, checkout/payment-window honesty)
— even though RBAC has zero prerequisites of its own and unblocks the most downstream work of anything in the
backlog (11 of 14 business areas). `next-up.md` and `index.md` both say this outright rather than quietly
picking one reading: the ranking follows the letter of the stated principle, and a reader who'd rather
optimize for unlock-radius should run unit 3 first. Two more mocked domains that are just as legitimate as
the four chosen (patient/care-records, partner-center) didn't fit the 3-5 slot cap and are named explicitly
as next-in-line rather than silently dropped.
### What was left undone, and why
- **Patient/care-records and partner-center de-mocks are not among the 5 `next-up.md` units** — both are
real candidates (see above), left out only by the slot cap, not by judgment that they matter less.
- **No code was written or changed** — pure roadmap synthesis, as scoped. The one exception is the BL-245
correction above, which is a fact-check, not a feature.
- **This phase did not re-audit `product/business/*` line by line for "specified but never built" beyond what
`backlog.md`'s Deferred section already carries** — a grep for `DEFERRED` across all 14 business docs
confirmed every theme they name (the 8 tables, holiday/surge pricing, GPS discovery, double-blind reviews,
tiered commission, organizations, etc.) already has a `BL-###` home. Re-deriving that reconciliation from
scratch would have been redoing phase 4's job, not phase 5's.
+140
View File
@@ -0,0 +1,140 @@
# Deferred — recorded, not re-decided
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> [documentation clean-up chain](../_plan/README.md).
**51 items** carry deferred status in [backlog.md](../status/backlog.md): the 43 in its dedicated
["Deferred" section](../status/backlog.md#deferred-43) (BL-220BL-262), plus 8 more filed by severity
elsewhere in the same file whose Status column also reads `deferred` (BL-050, BL-075, BL-082, BL-096,
BL-163, BL-168, BL-201, BL-219). This file **records them faithfully** — it does not re-litigate any of
them. A deferral with a trigger is a decision; every row below has one, preserved in substance from its
source. Grouped by theme, as backlog.md itself groups its Deferred section.
**How to read "size" below**: this column is this document's own estimate (S / M / L / XL), not carried from
any source file — backlog.md doesn't size deferred items, so sizing them is this phase's proposal, made to
help a future reader triage, and open to revision.
---
## The 8 unbuilt product tables
Named individually per the phase brief, cross-referenced from [implemented.md](../status/implemented.md):
| Table | Backs | BL-### | Pull-trigger |
| --- | --- | --- | --- |
| `organizations` | Employer/company account model | [BL-224](../status/backlog.md#deferred-43) | Product pulls it |
| `organization_nurses` | Employer↔nurse membership | [BL-224](../status/backlog.md#deferred-43) | Product pulls it |
| `fraud_flags` | ML-scored fraud signals | [BL-225](../status/backlog.md#deferred-43) | Product pulls it (manual suspension + support alerts cover this today) |
| `recurring_booking_schedules` | Recurring/subscription bookings | [BL-226](../status/backlog.md#deferred-43) | Product pulls it |
| `bnpl_settlement_entries` | Tranched BNPL settlement | [BL-227](../status/backlog.md#deferred-43) | Product pulls it, or a future BNPL provider tranches settlement (one settlement row covers today's provider) |
| `nurse_availability_slots` | Soft scheduling-guidance windows | [BL-228](../status/backlog.md#deferred-43) | Product pulls it |
| `nurse_availability_exceptions` | Time-off exceptions to the above | [BL-228](../status/backlog.md#deferred-43) | Product pulls it |
| `incidents` | First-class incident entity | [BL-244](../status/backlog.md#deferred-43) | Product pulls it (support alerts cover this today) |
All eight are, per refinement-phase-9's own framing (item 9.11 in that phase's handoff, distilled in
[decisions.md](../status/decisions.md)), **pure additive migrations** — nothing in the current schema needs
to change first when one is pulled forward. The same source item 9.11 also grouped in customer national-ID
KYC ([BL-229](#product-scope-cuts-not-building-yet-by-decision)) and geo bulk-import
([BL-230](#product-scope-cuts-not-building-yet-by-decision)) alongside the tables; backlog.md split those into
their own rows below since neither is a table.
---
## Scale-later infrastructure
Correct for a single instance / MVP load; each has a concrete, measurable trigger, not a date.
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-220](../status/backlog.md#deferred-43) — Redis (shared cache, cross-instance scheduler/money lock) not deployed | Every current seam (the in-process scheduler, rate limiting) is correct only for one running instance | Running more than one instance | M | refinement-phase 7/8/9 handoffs |
| [BL-221](../status/backlog.md#deferred-43) — Elasticsearch `INurseSearch` + outbox feeder not built | `SqlNurseSearch` is real, correct, and sufficient at MVP scale | SQL search shows real strain (latency/throughput on `nurse_search_index`) | L | backend-phase-7 follow-up, refinement-phase 9.7 |
| [BL-252](../status/backlog.md#deferred-43) — no short-TTL cache over hot search-result pages | Shipped no-cache at MVP; premature to cache before there's a measured hot path | Search read latency becomes a real problem | S | backend-phase-7 follow-up |
## Product-scope cuts (not building yet, by decision)
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-222](../status/backlog.md#deferred-43) — `IAnalyticsSink` / analytics warehouse not built | Events write to `ops.SystemEvents` (real, queryable) fire-and-forget; no cross-event pipeline yet | Product needs cross-event analytics beyond SQL queries | L | backend-phase-1, refinement-phase 9.8 |
| [BL-223](../status/backlog.md#deferred-43) — no real holiday-calendar feed | `IHolidayCalendar` reads a real, seeded, manually-maintained table; a yearly ops top-up is an acceptable MVP alternative to an automated feed | Product pulls it (manual refresh becomes a burden) | M | backend-phase-1, refinement-phase 9.9 |
| [BL-229](../status/backlog.md#deferred-43) — customer national-ID KYC collection not built | Deliberate: never gate browsing/booking on it | **Never, by design** — informational only | — | backend-phase-3, refinement-phase-9, business/01 |
| [BL-230](../status/backlog.md#deferred-43) — geography bulk-import feed (`IGeoDataImporter`) not built | The idempotent seed + admin CRUD is sufficient for MVP | Product pulls it | M | backend-phase-4 follow-up |
| [BL-231](../status/backlog.md#deferred-43) — holiday/surge pricing, a distinct Companionship tier, tiered per-category commission | All explicitly out of MVP scope in business/03 | Product pulls it | M (pricing rule) / L (new tier) | backend-phase-5 follow-up, business/03 |
| [BL-232](../status/backlog.md#deferred-43) — GPS-radius "nurses near me" map discovery | Coverage stays named-district-only by design | **None — permanent product decision** | — | backend-phase 4/7, business/04 |
| [BL-233](../status/backlog.md#deferred-43) — automated MoH/INO license lookup + professional-liability-insurance step type | Manual verification is the MVP path; no B2B lookup API is confirmed to exist yet | A lookup portal is confirmed to exist, or product pulls the insurance step | M | backend-phase-6, business/02 |
| [BL-239](../status/backlog.md#deferred-43) — dedicated MoR center-settlement payout path (تسهیم split leg) | Per decision 6.6; the platform pays nurses directly today regardless of MoR | Product pulls it | M | refinement-phase 6/8 follow-ups |
| [BL-241](../status/backlog.md#deferred-43) — on-demand/instant nurse payout, per-nurse payout frequency | MVP is one fixed weekly cadence for everyone | Product pulls it | M | backend-phase-13, business/10 |
| [BL-242](../status/backlog.md#deferred-43) — automated clawback recovery beyond next-batch netting | Simple greedy netting covers MVP | Product pulls it | M | backend-phase-13 |
| [BL-243](../status/backlog.md#deferred-43) — two-way double-blind reviews with timed reveal | One-way customer review is MVP scope per business/11 | Product pulls it | L | backend-phase-14, business/11 |
| [BL-246](../status/backlog.md#deferred-43) — automated eNamad/MoH license verification for partner centers | Manual-approve is MVP scope per business/13 | Product pulls it | M | backend-phase-15 |
| [BL-247](../status/backlog.md#deferred-43) — no telephony seam for emergencies | The emergency contact is an out-of-platform `tel:` link, by deliberate design | **None — permanent product decision** | — | backend-phase-15 |
| [BL-248](../status/backlog.md#deferred-43) — SMS/push notification channels not built | Only in-app notifications are real; deliberate MVP scope (business/14) | Notification UX demands out-of-app reach | M | refinement-phase-9, refinement-phase 9.10 |
| [BL-251](../status/backlog.md#deferred-43) — PWA/offline caching (Workbox) unbuilt | Marked "maybe" in the original product backlog — optional from the start | Product pulls it | M | product/notes/open-questions.md |
| [BL-254](../status/backlog.md#deferred-43) — partner dashboard's sponsored-nurse list capped at 50, no pagination | No center has exceeded 50 sponsored nurses yet | A center exceeds 50 sponsored nurses | S | backend-phase-15 follow-up |
| [BL-257](../status/backlog.md#deferred-43) — no true desktop search layout | The phone-width frame is the whole app's deliberate design; a full responsive pass was explicitly deferred post-chain | Product decides to support desktop | L | ui-phase-12 follow-up |
| [BL-258](../status/backlog.md#deferred-43) — Persian OG image is Latin-only (also [BL-202](../status/backlog.md#minor-115)) | Deliberate scope cut on `/welcome` and other share cards | Persian social sharing becomes a priority | S | ui-phase-13 follow-up |
| [BL-259](../status/backlog.md#deferred-43) — no list-row EVV "currently checked in" indicator | Needs a product/API decision to avoid an N+1 read; not free to add | Product prioritizes a list-level EVV signal | S | ui-phase-5 follow-up |
| [BL-260](../status/backlog.md#deferred-43) — no web-push for new nurse booking requests | The 15-second poll is the only freshness mechanism today | Push infra (service worker + VAPID + dispatch rail) is built | L | ui-phase-7 follow-up, REQ-054 |
| [BL-262](../status/backlog.md#deferred-43) — no real payment-gateway/Shaparak logos near the pay CTA | No licensed assets yet; a generic lock-icon notice stands in | Licensed gateway assets obtained | S | ui-phase-6 follow-up |
## Vendor / integration deferrals
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-235](../status/backlog.md#deferred-43) — SMS.ir/Ghasedak adapters not built | Only Kavenegar's real-SMS path is wired; either alternative throws at startup by design rather than silently mocking | A second SMS vendor is needed | M | refinement-phase-8 follow-up |
| [BL-236](../status/backlog.md#deferred-43) — Finnotech/Moadian token-exchange refresh + signing certificate not wired | Both are deploy-time actions that need real credentials, which don't exist yet | Going to a real Moadian integration ([pre-launch.md §5](pre-launch.md#5-legal--tax-items-that-are-code-side)) | S once credentials exist | refinement-phase-8 handoff |
| [BL-238](../status/backlog.md#deferred-43) — per-provider-code BNPL revert incomplete (always drives SnappPay) | Only one BNPL provider is live today | A second BNPL provider goes live | S | refinement-phase-8 follow-up |
## Scheduled-ops deferrals (manual today, by choice or by not-yet-built)
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-234](../status/backlog.md#deferred-43) — credential-expiry scan + EVV no-show sweep are manual-only endpoints | No cron calls either yet | A scheduled-ops phase | S | backend-phase 6/9 follow-ups |
| [BL-237](../status/backlog.md#deferred-43) — Moadian reconciliation poll + refund-settlement poll are manual | No scheduled cron for either | A scheduled-ops phase | S | backend-phase-11, refinement 6/7/8 follow-ups |
| [BL-240](../status/backlog.md#deferred-43) — payout batch *generation* is automatic; *processing* a batch stays a deliberate, explicit admin action | Not a bug — moving real money should have a human in the loop at this scale | **None — permanent product decision** | — | refinement-phase-7, backend-phase-13 |
## Cleanup-of-convenience and data-integrity hygiene
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-219](../status/backlog.md#minor-115) — rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel | The name is a load-bearing sentinel `StartupSecretsGuard` and the pre-commit hook both check for; renaming touches 7 files for a cosmetic gain | None — cleanup-of-convenience | S | phase 2 (`open-contradictions.md` C-2) |
| [BL-250](../status/backlog.md#deferred-43) — the ESLint unused-vars gate is a repo-wide no-op | Config patches an export path that doesn't carry the rule; fixing it is a dedicated infra task, not a drive-by | A dedicated infra task | S | frontend-phase-13 follow-up |
| [BL-253](../status/backlog.md#deferred-43) — payment-webhook confirm path uses two DB commits instead of one transaction | Kept safe today via idempotency + a forward-only guard; a real fix needs `IUnitOfWork` to grow a transaction scope first | `IUnitOfWork` grows a transaction scope | M | backend-phase-10 follow-up |
| [BL-255](../status/backlog.md#deferred-43) — `Bookings`/`Invoices.partner_center_id` have no DB-level FK | Only `nurse_profiles.partner_center_id` got one, per that phase's own Definition of Done | A data-integrity pass on partner-center columns | S | backend-phase 11/15 follow-ups |
| [BL-256](../status/backlog.md#deferred-43) — skeleton→content crossfade never retrofitted onto every list/detail page | Exists as a one-line-per-screen pattern; applying it everywhere is a dedicated visual pass, not a drive-by | A dedicated visual-polish pass | M | ui-phase-12 follow-up |
| [BL-249](../status/backlog.md#deferred-43) — legacy `UserRefreshTokens` (gRPC auth path) still exists alongside real session-based REST auth | Removing it means confirming nothing still depends on the gRPC path | gRPC moves to sessions, or is dropped | S | backend-phase-2 follow-up |
---
## Deferred at the item level — gated behind a console that isn't prioritized yet
These 7 are filed by severity (major/minor) rather than in backlog.md's thematic Deferred section, because
each is a specific, already-scoped gap rather than a standing product decision — but each is genuinely blocked
on the same thing: an admin console this roadmap hasn't prioritized building yet. Recorded here for the same
reason: each has a trigger, and none should be picked up piecemeal ahead of its console.
| Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- |
| [BL-050](../status/backlog.md#major-86) — no preview/approve/reject route for admin refunds (`POST admin_refunds` creates+executes in one call) | The three-step admin `RefundPanel` console has no real backend to call | Admin refund console prioritized | M | phase 3 flow gap |
| [BL-075](../status/backlog.md#major-86) — `recordTransferReference` targets a route that doesn't exist | The payout batch-detail reconcile field would 404 on flip | Payout reconciliation console prioritized | S | phase 3 flow gap, REQ-036 |
| [BL-082](../status/backlog.md#major-86) — `approveVerification`/`rejectVerification`/`getDocumentSignedUrl` target routes that don't exist | The admin verification case page's approve/reject CTAs would 404 the moment BL-001 (RBAC) is fixed | Verification-admin console prioritized | M | phase 3 flow gap, REQ-034 |
| [BL-096](../status/backlog.md#major-86) — tier (c) of the public front door (guest search + public nurse profiles) unbuilt | Needs a backend phase (anonymous rate-limited search read, privacy-reviewed profile shape) **and** an explicit privacy sign-off on the nurse-profile field list — a decision, not a coding task | Privacy sign-off obtained **and** REQ-066/REQ-067 delivered | L | ui-phase-13, product/notes/open-questions.md |
| [BL-163](../status/backlog.md#minor-115) — `POST tickets/{id}/assign` is a phantom endpoint the client already targets | Client-side assign UI exists; server route doesn't | Ticket assignment prioritized | S | phase 3 flow gap, REQ-063 |
| [BL-168](../status/backlog.md#minor-115) / [BL-261](../status/backlog.md#deferred-43) — ticket attachments are fully designed/built client-side, gated off behind a flag | Waiting on the upload/signed-URL backend | Attachment backend delivered | M | phase 3 flow gap + REQ-060 (**note:** these are the same underlying fact filed as two BL-### by two different harvest passes — treat as one item when picking it up) |
| [BL-201](../status/backlog.md#minor-115) — center self-onboarding (write-then-masked IBAN) never exercised on a real route | Deferred pending partner-center de-mock generally | Center self-service onboarding prioritized | M | phase 3 flow gap |
---
## Verification note
[BL-245](../status/backlog.md#deferred-43) is not listed above: its own trigger was "phase 5 verification
pass," which this phase executed directly against the code rather than deferring further — see
[decisions.md](../status/decisions.md) for the finding (2 of 3 admin actions confirmed built; only
`FlagConcern` is a genuine, small, unfiled gap).
## What this file is not
This is a record, not a plan — nothing here is re-ranked or newly triggered by this phase. The proposed order
of work that **isn't** deferred is [next-up.md](next-up.md); what the deferrals above cost to keep deferring
is partly covered in [tech-debt.md](tech-debt.md) where a deferral and a debt item are the same underlying
fact (e.g. Redis/BL-220 appears in both, because "not built yet" and "costs more the longer it's not built"
are two different questions about the same gap).
+79 -18
View File
@@ -1,27 +1,88 @@
# Roadmap — where it goes next # Roadmap — where it goes next
> **Populated by phase 5 — not yet written.** Phase 5 depends on phase 4: you cannot sequence work > Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> that has not been reconciled into one backlog. > [documentation clean-up chain](../_plan/README.md).
A **proposal**, not a commitment. The ordering is reasoned and can be overruled; what is not negotiable A **proposal**, not a commitment. The ordering is reasoned and can be overruled; what is not negotiable is
is that every item traces to a `BL-###` in [status/backlog.md](../status/index.md), so nothing is that every item traces to a `BL-###` in [status/backlog.md](../status/backlog.md), so nothing here was
scheduled that was not first triaged. invented — it was triaged first, in phase 4.
## Planned contents **This page marks its own judgement calls.** Two kinds of claim appear below: **recorded** (came from
`backlog.md`, `CLAUDE.md`, `DEPLOY.md`, or a `product/` decision — check the source, not this page's
reasoning) and **proposed** (this phase's own synthesis — the ranking, the grouping into units, the sizing).
Where the two could be confused, it's labeled.
| File | Covers | ## The four documents
| --- | --- |
| `next-up.md` | The next 35 units of work, each spec'd enough to start |
| `deferred.md` | Recorded, each with the trigger that pulls it forward |
| `tech-debt.md` | What must be paid before scale, and what deferring it costs |
| `pre-launch.md` | The hard gate before real users touch this |
## One item already known for `pre-launch.md` | File | Answers | Ordered? |
| --- | --- | --- |
| [pre-launch.md](pre-launch.md) | What must be true before real money moves | Yes — but it's a **gate**, not a priority list. Everything in it blocks launch; none of it is optional |
| [next-up.md](next-up.md) | What to build next, and why that and not something else | Yes — 5 units, sequenced by the principle below |
| [deferred.md](deferred.md) | What's deliberately not being built, and what would change that | No — a record, picked up by its own trigger, not a schedule |
| [tech-debt.md](tech-debt.md) | What's getting more expensive the longer it waits | No — each item has its own trigger; not a queue |
---
## The sequencing principle
**Recorded** — stated in the phase brief that produced this roadmap, not derived by this phase:
> The product's own promise is trust-first and money-holding. So the order is (1) anything that makes a
> *money or trust* flow lie to a user, (2) anything that makes a built flow unusable, (3) anything that makes
> a mocked flow real, (4) new surface area.
This principle was given, not derived — it isn't this phase's invention. **Applying it to the actual 262 open
backlog items** — which item is a "lie" versus merely "unusable," which five units to carve out of 86 major
items, how to size each — **is this phase's proposal**, in [next-up.md](next-up.md).
## The proposed order — **this phase's proposal**
| # | Unit | Category | Closes (blockers) | Size |
| --- | --- | --- | --- | --- |
| 1 | [Nurse verification stops lying about status](next-up.md#unit-1--nurse-verification-stops-lying-about-verification-status) | 1 — trust lie | BL-010 | L |
| 2 | [Checkout stops lying about money and time](next-up.md#unit-2--checkout-and-the-payment-window-stop-lying-about-money-and-time) | 1 — money lie | BL-006 | M |
| 3 | [Admin RBAC: give seeded admins the claim the code checks for](next-up.md#unit-3--give-the-seeded-admins-the-claim-the-code-already-checks-for) | 2 — unusable | BL-001, BL-002 | M |
| 4 | [De-mock refunds and BNPL](next-up.md#unit-4--de-mock-refunds-and-bnpl-onto-the-server-thats-already-built) | 3 — mocked→real | BL-007, BL-008, BL-009 | L |
| 5 | [De-mock nurse payouts](next-up.md#unit-5--de-mock-nurse-payouts-including-the-one-action-with-no-ui-at-all) | 3 — mocked→real | BL-012, BL-016 | L |
**One tension, stated plainly rather than hidden:** unit 3 (RBAC) has zero prerequisites and unlocks more
downstream work than any other single item — 11 of 14 business areas, per
[implemented.md](../status/implemented.md#cross-cutting-the-rbac-finding). A reader who weighs "unlocks the
most" above "is technically a lie versus merely unusable" would be right to run it first. It's ranked third
here because the stated principle was applied literally. **This is exactly the kind of call this document
exists to make visible and overridable** — see [next-up.md](next-up.md) for the full reasoning per unit,
including two strong candidates (patient/care records, partner-center) that didn't fit the 3-5 slot cap and
are named as the next two in line.
Running in parallel with all five, on its own track: everything in [pre-launch.md](pre-launch.md). None of it
is feature work, so none of it competes with the units above for the same engineering time in the same way —
but none of it is optional before real users arrive, regardless of which unit above ships first.
## What we're not doing, and why — pointer
The standing answer to "what are we not doing" is [deferred.md](deferred.md): **51 items** (43 recorded as a
themed group in `backlog.md`, 8 more filed by severity elsewhere but also carrying deferred status), each with
the trigger that would pull it forward, plus the **8 unbuilt product tables** named individually. None of them
were re-decided here — phase 5's job was to record them faithfully, not re-litigate phase 4's or `product/`'s
calls. The one exception: [BL-245](../status/backlog.md#deferred-43)'s own trigger named this phase directly
("phase 5 verification pass"), so it was executed rather than deferred again — see
[decisions.md](../status/decisions.md) for the finding.
## One item already known for `pre-launch.md` — **recorded**, from root CLAUDE.md and DEPLOY.md
The repo **contains live credentials on purpose** — a deliberate pre-launch trade for a demo deployment, The repo **contains live credentials on purpose** — a deliberate pre-launch trade for a demo deployment,
documented in [root CLAUDE.md §6](../../CLAUDE.md) and [DEPLOY.md](../../DEPLOY.md). Before onboarding documented in [root CLAUDE.md §6](../../CLAUDE.md) and [DEPLOY.md](../../DEPLOY.md). Before onboarding real
real users those values must be rotated and the secret half moved out of git. users those values must be rotated and the secret half moved out of git.
One value is load-bearing and must **never** change: `Seams:FieldEncryption:Key` and `:HashKey` decrypt One value is load-bearing and must **never** be rotated in place: `Seams:FieldEncryption:Key` and `:HashKey`
every encrypted column and derive the `PhoneHash` that every login looks up. Rotating them locks every decrypt every encrypted column and derive the `PhoneHash` every login looks up. Changing them without a
account out. migration locks every account out — [pre-launch.md §1](pre-launch.md#1-rotate-the-committed-credentials)
spells out what that migration actually requires, since "never change it" and "never be able to change it"
are different problems.
## How to use this page
Pick the next piece of work from the table above, or from [pre-launch.md](pre-launch.md) if a launch date is
what's driving the decision. If neither fits, [tech-debt.md](tech-debt.md) names what's quietly getting more
expensive, and [deferred.md](deferred.md) names what's already been decided against — check there before
proposing something new, since it might already have a recorded reason and a trigger.
+210
View File
@@ -0,0 +1,210 @@
# Next up — the opinionated part
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> [documentation clean-up chain](../_plan/README.md).
Five units, each spec'd enough that a fresh agent session could start one without re-deriving context. The
order applies the sequencing principle from [index.md](index.md#the-sequencing-principle): (1) stop a money
or trust flow from lying to a user, (2) fix a built flow that's unusable, (3) make a mocked flow real, (4) new
surface area. Nothing here conflicts with any `product/` business rule — every unit below is closing an
implementation gap against a rule the business docs already decided, not proposing new behavior.
---
## Unit 1 — Nurse verification stops lying about verification status
**Goal.** Flip the `verification` client domain's mock off for the nurse-facing and public/search-visibility
surfaces, so the app renders the server's real, already-correct status everywhere it matters: the nurse's own
verification screen, the activation checklist's "go live" gate, search's `is_searchable` filter, and the
public trust badge.
**Why now.** Verification *is* the platform's trust promise. Today the mock doesn't show stale data — it
actively contradicts a real, server-computed truth: a server-verified nurse renders as unverified everywhere
the client reads her status ([BL-010](../status/backlog.md#blockers-18)). Under the sequencing principle this
is a category-1 item: a trust flow lying to a user, ranked above anything merely unbuilt.
**What it unblocks.** A trustworthy "go live" CTA; the real search-visibility gate becomes observable instead
of hidden behind fabricated client state; closes the one confirmed production-seam breach
(`nurse/verification/page.tsx` unconditionally imports the mock module regardless of its render gate —
[BL-080](../status/backlog.md#major-86)); establishes the de-mock pattern units 4 and 5 reuse.
**Technical prerequisites.** None blocking. Scope this unit to the nurse-facing and public halves only — the
admin approve/reject/suspend actions stay behind
[BL-082](../status/backlog.md#major-86) (the routes don't exist yet, independent of RBAC), and are naturally
picked up alongside unit 3 once both the routes and RBAC exist.
**Affected flows.** [nurse-verification](../flows/nurse-verification.md) (mocked → partial),
[onboarding-nurse](../flows/onboarding-nurse.md), [search-and-discovery](../flows/search-and-discovery.md)
(real gate becomes visible), [public-front-door](../flows/public-front-door.md) (trust badge becomes real).
**Rough size:** L — a 100%-mocked, 14-operation domain with real DTO reconciliation.
**Backlog ids closed:** [BL-010](../status/backlog.md#blockers-18) (blocker), BL-080, BL-081, BL-084, BL-190,
BL-191, BL-192, BL-214. Worth fixing in the same pass, same trust-truth theme, different root cause:
[BL-098](../status/backlog.md#major-86) (an unverified nurse's profile is still readable and asserts
"verified" anonymously by id). BL-083 (admin catalog UI for suspend/scan-expiring) is explicitly out of scope
— it's an admin surface, deferred with unit 3.
---
## Unit 2 — Checkout and the payment window stop lying about money and time
**Goal.** Fix the cluster of checkout-and-payment defects that tell a customer or nurse something false: the
timezone-less deadline that can render a 30-minute window as ~4 hours and expire silently
([BL-006](../status/backlog.md#blockers-18)), the checkout summary's missing `nurseVerified` field
(BL-058), the payment confirmation that can't deep-link to its booking and hides the tracking code
(BL-059), and the invoice screen's client-computed row that's wrong by exactly the VAT amount instead of
showing the server's real total (BL-060). Close the two related blind spots while in the same code: no
payment-history read (BL-061) and no escrow-ledger read surface at all (BL-062).
**Why now.** Category-1 by the letter of the sequencing principle: a countdown that shows hours when minutes
remain, and an invoice wrong by a fixed, known amount, are exactly "a money/trust flow lying to a user" —
not a missing feature, a false one.
**What it unblocks.** The one flow every paying customer touches becomes honest; removes a standing violation
of the repo's "client never computes money" rule ([BL-060](../status/backlog.md#major-86), called out
directly in [decisions.md](../status/decisions.md)); a trustworthy countdown is also a quiet prerequisite for
ever safely flipping [BL-005](../status/backlog.md#blockers-18) (the dead card-payment rail) to a real PSP —
no reason to give a real gateway a lying clock.
**Technical prerequisites.** None. Every item here is a DTO addition or a client display fix; nothing depends
on RBAC or a mock flip elsewhere.
**Affected flows.** [checkout-and-payment](../flows/checkout-and-payment.md),
[booking-request](../flows/booking-request.md) (shares the deadline field).
**Rough size:** M. Mostly additive DTO fields plus client display fixes; budget extra time for BL-006 since
`DateTime``DateTimeOffset` touches a shared type used beyond this one screen.
**Backlog ids closed:** [BL-006](../status/backlog.md#blockers-18) (blocker), BL-042, BL-058, BL-059, BL-060,
BL-061, BL-062.
---
## Unit 3 — Give the seeded admins the claim the code already checks for
**Goal.** `DynamicPermissionService.CanAccess` grants only the literal role `admin`; no seeded account holds
it, so every seeded admin 403s on every `DynamicPermission`-gated controller
([BL-001](../status/backlog.md#blockers-18)). Either broaden the check to also honor the roles actually
seeded (`super_admin`/`finance`), or add the missing step that writes a `DynamicPermission` claim when those
roles are granted — and seed at least one account holding the literal role so the fix is testable out of the
box ([BL-002](../status/backlog.md#blockers-18)).
**Why now.** Category-2 under the stated principle (it makes a built, real, already-coded admin backoffice
completely unusable, rather than lying about anything) — but it is the single highest-leverage fix in the
whole backlog: one root cause independently degrades **11 of 14 business areas**
([implemented.md](../status/implemented.md#cross-cutting-the-rbac-finding)). Worth naming the tension
directly: nothing stops this from running before units 1-2 except the sequencing principle's letter — it has
zero prerequisites of its own, same as they do. It's ranked third here because the principle is applied
literally; a reader who weighs "unlocks the most downstream work" more heavily than the letter of the
principle would be right to run this first, and that's exactly the kind of overrule this document exists to
make easy.
**What it unblocks.** Admin verification review (once BL-082's routes exist), refund preview/approve/reject
(unit 4), payout `mark_failed` and batch processing (unit 5), reviews ever leaving moderation
([BL-017](../status/backlog.md#blockers-18)), ticket assignment, cancellation-policy admin edits, the admin
user/role directory ([BL-029](../status/backlog.md#major-86)) — essentially every admin-side item in units
4-5 and in [deferred.md](deferred.md#deferred-at-the-item-level--gated-behind-a-console-that-isnt-prioritized-yet).
**Technical prerequisites.** None.
**Affected flows.** [admin-backoffice](../flows/admin-backoffice.md) (mocked → real), plus the admin half of
nurse-verification, cancellation-and-refunds, messaging-tickets, nurse-earnings-and-payouts, partner-center,
reviews, booking-request, booking-lifecycle-evv, and nurse-catalog-and-pricing — the 11 areas
[implemented.md](../status/implemented.md#cross-cutting-the-rbac-finding) names.
**Rough size:** M. The fix itself is small and surgical; the size is in re-verifying admin surfaces across 11
areas afterward, since several have never been reachable long enough to know what else breaks once they 200.
**Backlog ids closed:** [BL-001](../status/backlog.md#blockers-18), BL-002 (both blockers). Directly unblocks
without itself closing: BL-017, BL-029, BL-050, BL-082, BL-116, BL-117, BL-125, BL-163.
---
## Unit 4 — De-mock refunds and BNPL onto the server that's already built
**Goal.** Flip the `refunds` and `bnpl` client domains off their mocks. Both currently read a retired/wrong
bookings-mock store that would break, not just look stale, on a naive flip: refunds would render a
10000%-scale refund amount ([BL-009](../status/backlog.md#blockers-18)); the BNPL wizard 404s for every real
booking id ([BL-008](../status/backlog.md#blockers-18)). Seed a real `Bnpl` payment-gateway row so BNPL calls
stop 400ing ([BL-007](../status/backlog.md#blockers-18)). Fix the DTO drift the flow atlas already catalogued
— BL-047 (refund channel pinned to a fixture id), BL-048 (client discards six real fee-split fields), BL-049
(three enum mismatches) — *before* flipping, not after, exactly the order phase 3/4's analysis already sets up.
**Why now.** Both are money flows. Both are among the five domains
[implemented.md](../status/implemented.md) flags as most at risk of breaking, not just showing stale data, on
a flip — the reconciliation work is already scoped by the flow docs, so this is executing a known plan, not
discovering one.
**What it unblocks.** A customer sees a real cancellation/refund preview instead of a fabricated one; the
platform's second payment rail (BNPL) becomes exercisable end-to-end locally for the first time.
**Technical prerequisites.** None to start the customer-facing halves. The admin sides —
[BL-050](../status/backlog.md#major-86) (refund preview/approve/reject route doesn't exist) and the
BNPL-adjacent admin surfaces — stay deferred regardless of this unit, consistent with their own
already-recorded "gated on a console" status in [deferred.md](deferred.md).
**Affected flows.** [cancellation-and-refunds](../flows/cancellation-and-refunds.md) (mocked → partial),
[bnpl-installments](../flows/bnpl-installments.md) (mocked → partial).
**Rough size:** L. Two domains, DTO reconciliation on both, a gateway-seeding change. Worth solving
[BL-131](../status/backlog.md#deferred-43) (nothing fires the BNPL webhook locally) in the same pass — without
it, a real BNPL order can be initiated but never observed reaching `settled` in dev.
**Backlog ids closed:** [BL-007](../status/backlog.md#blockers-18), BL-008, BL-009 (all three blockers),
BL-047, BL-048, BL-049, BL-052, BL-053, BL-054, BL-126, BL-127, BL-128, BL-129, BL-130.
---
## Unit 5 — De-mock nurse payouts, including the one action with no UI at all
**Goal.** Flip the `payouts` client domain to real. Build the client UI for `process` — the irreversible step
that actually executes a payout batch, which currently has **zero client caller anywhere in the app**. Fix
`DeriveEarningsState`'s wrong "paid" logic, which marks a booking paid whenever it merely has a payout *link*,
regardless of that payout's actual status, and reconcile the four earnings buckets against the ledger
([BL-012](../status/backlog.md#blockers-18)).
**Why now.** This is the flow where a nurse's trust in ever actually getting paid lives, and it currently
tells her she's been paid when the payout may not have succeeded — a money lie hiding inside a mocked flow.
It's category-3 (de-mock) and category-1 (stop a lie) at the same time, which is why it's grouped with the
other de-mock units rather than ranked above unit 3 on category-1 grounds alone: fixing the lie and fixing the
mock are the same commit here, unlike units 1-2 where the lie could be fixed without a mock flip.
**What it unblocks.** A truthful earnings/payout history for nurses; the one missing admin action (batch
processing); [BL-076](../status/backlog.md#major-86) (`mark_failed` has no client op) becomes reachable once
this unit and unit 3 (RBAC) have both landed.
**Technical prerequisites.** Unit 3 (RBAC), for the admin-side `process`/`mark_failed` actions specifically —
the nurse-facing earnings/history half has no such dependency and can proceed independently. This is the one
unit in this list with a real cross-unit dependency; sequence its admin half after unit 3 regardless of where
the two land in a sprint.
**Affected flows.** [nurse-earnings-and-payouts](../flows/nurse-earnings-and-payouts.md) (mocked → partial).
Worth fixing [BL-016](../status/backlog.md#blockers-18) in the same pass — a booking swept to `missed` never
reaches a payable state today, so it can never enter a payout batch regardless of this unit's other fixes.
**Rough size:** L. Mock removal, a wholly new admin-action UI, a state-derivation bug fix, and a
ledger-reconciliation check.
**Backlog ids closed:** [BL-012](../status/backlog.md#blockers-18), BL-016 (both blockers), BL-073, BL-074,
BL-076, BL-179, BL-180, BL-181, BL-182, BL-183, BL-184. Unblocks without closing: BL-075.
---
## Considered, and deliberately not in this list
- **[BL-013](../status/backlog.md#blockers-18) — partner-center (100% mocked, zero tenancy).** Just as mocked
as the four domains above, but structurally different: 5 core routes
(`centers/me`, `/me/nurses`, `/me/bookings`, `/me/bookings/{id}`, `/me/settlement`) **don't exist
server-side at all**. This isn't a de-mock, it's a small backend phase followed by a de-mock — a bigger unit
than the others, and one where the "flip the mock" playbook from units 1/4/5 doesn't directly apply. Next
in line after these five.
- **[BL-011](../status/backlog.md#blockers-18) — patient/care records (100% mocked, server real).** Same
shape as units 1, 4, and 5 (server real, client mocked, DTO drift already catalogued) and just as legitimate
a candidate — it didn't fit this document's 3-5 cap, not a judgment that it matters less. Directly next in
line if a sixth unit is wanted.
- **[BL-096](../status/backlog.md#major-86) — tier (c) of the public front door (guest search + public nurse
profiles).** New surface area (category 4), and explicitly named in the phase brief as ranking below making
the authenticated flows honest regardless of visibility. Also blocked on a decision this document can't
make: an explicit privacy sign-off on the nurse-profile field list, plus REQ-066/067 delivered. Stays in
[deferred.md](deferred.md), not here.
+177
View File
@@ -0,0 +1,177 @@
# Pre-launch — the hard gate before real users touch this
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> [documentation clean-up chain](../_plan/README.md).
Everything below must be true before a real user with real money uses the platform. This is a gate, not a
backlog — items here are not ranked by convenience, they're ranked by what "real user, real money" requires.
Four things put you here: **committed credentials**, **mocked money/identity rails**, **Development running in
production**, and **legal/tax code that isn't finished**. A fifth — **every Phase 4 blocker** — is included
because "blocker" means *the product is wrong or unusable*, and that bar applies with or without real money.
---
## 1. Rotate the committed credentials
The repo contains live credentials **by deliberate pre-launch decision**
([root CLAUDE.md §6](../../CLAUDE.md), [BL-003](../status/backlog.md#blockers-18)): DB `sa`, both
JWE/field-encryption key halves, Kavenegar, Neshan, Finnotech, and the Telegram bot token. All of them must be
rotated and the secret half moved out of git before onboarding real users.
| Credential | Rotation is | What it takes |
| --- | --- | --- |
| DB `sa` password | a straight rotation | Change on the SQL Server, update `appsettings.Production.json`. No data migration. |
| Kavenegar / Neshan / Finnotech API keys | a straight rotation | Issue new keys with each vendor, update config. No data migration. |
| Telegram bot token | a straight rotation | Regenerate via BotFather, update `docker-compose.yml` (`otp-relay.environment`) and the appsettings copy — the two must match ([DEPLOY.md](../../DEPLOY.md)). |
| `IdentitySettings:SecretKey` / `Encryptkey` (JWE) | a straight rotation | Safe to rotate any time — the only side effect is signing everyone out. |
| **`Seams:FieldEncryption:Key` / `:HashKey`** | **not** a rotation — see below | **Never** change without the migration in the next section. |
### The one item that is a project, not a config edit
`Seams:FieldEncryption:Key` decrypts every encrypted column (phone numbers, addresses, IBANs, clinical
notes); `:HashKey` derives `users.PhoneHash`, which every login looks up. Changing either value in place makes
all existing encrypted data unreadable and locks every account out — this is stated as a permanent invariant
in [DEPLOY.md](../../DEPLOY.md) and [decisions.md](../status/decisions.md), and it is correct **as long as the
key never needs to change**. It will need to change eventually (security incident, key-management policy,
routine hygiene), and at that point it is its own migration project:
1. Decrypt every affected column with the old key, in place, inside a maintenance window or behind a
dual-key read path.
2. Re-encrypt with the new key.
3. Recompute `PhoneHash` for every user with the new `HashKey`, since it's a one-way derivation — there is no
way to "re-key" a hash without the plaintext.
4. Decide the cutover strategy: a maintenance-window rewrite (simplest, requires downtime sized to the data
volume) versus a dual-read migration (no downtime, more code to write and then delete).
**Rough size: large.** Nothing about this is hard, but nothing about it is a config edit either — track it as
its own piece of work with its own testing pass, not a line item inside a generic "rotate credentials" task.
---
## 2. Still-mocked production seams
Exactly one `Seams:*:Provider` is set anywhere in the repo — `Seams:Sms:Provider = telegram`, and the Telegram
relay is [documented as the pre-launch demo rail](../../CLAUDE.md), not a production SMS gateway. Every other
rail runs on its mock, in both development and the deployed stack
([docs/flows/index.md](../flows/index.md#mock-vs-real-map--server-seams)):
| Rail | Mock today | Real adapter | What flipping takes |
| --- | --- | --- | --- |
| SMS / OTP | `LoggingSmsSender` (real path is `telegram`, a demo relay) | `KavenegarSmsSender` — already coded | Set `Seams:Sms:Provider = kavenegar` + real API key/sender. Attempting `smsir`/`ghasedak` throws at startup by design — those adapters don't exist ([BL-235](../status/backlog.md#deferred-43)). |
| Card PSP | `MockPaymentProvider` (redirects to a non-existent host — [BL-005](../status/backlog.md#blockers-18)) | `ZarinPalPaymentProvider` — already coded | Real merchant credentials from a licensed PSP — which requires e-namad (§5) — plus a reachable webhook endpoint. **The selector is a plain string check, not an enum** — any non-`mock` value silently selects ZarinPal, so a typo in this setting picks a real gateway by accident. |
| Settlement split (تسهیم) | `MockSettlementSplitProvider` | `ProviderSettlementSplitProvider` — already coded | Real settlement-provider credentials. |
| BNPL | `MockBnplProvider` (no gateway row even seeded — [BL-007](../status/backlog.md#blockers-18)) | `SnappPayBnplProvider` / `DigipayBnplProvider` — already coded | Real provider credentials, a seeded `Bnpl` gateway row, and a webhook receiver reachable from the provider (nothing fires it in dev today — [BL-131](../status/backlog.md#minor-115)). |
| Object storage | `LocalDiskObjectStorage` (`GetUrl` returns a `file://` URI no browser can fetch — [BL-024](../status/backlog.md#major-86)) | `S3ObjectStorage` — already coded | A real bucket + credentials. Flipping this also happens to fix BL-024, since S3's `GetUrl` returns an actual HTTP(S) URL. |
| Geocoder | `MockGeocoder` (server) / keyless grid stand-in (client, `NEXT_PUBLIC_NESHAN_KEY` unset — [BL-025](../status/backlog.md#major-86)) | `NeshanGeocoder` — already coded | A real Neshan API key on **both** sides — the client key is separate from any server-side one. |
| **Bank transfer (payouts)** | `MockBankTransferProvider` | **none — moves no money** | Nothing to flip. This is the one rail with no real adapter written at all; even after every other rail above goes real, **payouts still cannot move real money** until a real bank-transfer integration is built from scratch. Size this as new work, not a config change. |
| e-invoicing (مودیان) | `MockMoadianClient` | `MoadianClient` — already coded | Token-exchange refresh and the Moadian signing certificate are not wired ([BL-236](../status/backlog.md#deferred-43)) — both are deploy-time actions once real credentials exist, not code changes. |
| Shahkar · e-KYC · IBAN ownership | mocks | Finnotech adapters — already coded | Real Finnotech credentials. |
| Credential (MoH/INO) · eNamad · review moderation | mock, always | **none — deliberate** | These stay manual/mocked by product decision, not a launch gap — see [deferred.md](deferred.md). |
| Search | *(no mock)* | `SqlNurseSearch` | Already real. Not a launch item. |
**Net: of 10 rails with a real adapter, 9 are a credentials-and-config flip; 1 (bank transfer) doesn't have a
real adapter yet.** Two rails throw at startup rather than silently falling back to the mock if misconfigured
(SMS providers other than Kavenegar, and any `Search:Backend` other than `sql`) — that fail-fast behavior is
intentional and should stay.
---
## 3. Running as Development in production
[DEPLOY.md](../../DEPLOY.md) documents this as a **deliberate trade** for a demo deployment among trusted
people. Every consequence below stops being acceptable the moment strangers can reach the site:
| Consequence | Why it matters | Fix |
| --- | --- | --- |
| The developer exception page is public | Any unhandled 500 on `api.balinyaar.ir` returns a stack trace and configuration detail to the caller | Switch to `Production` environment |
| `GET /api/v1/dev/last_otp/{phone}` is live and anonymous ([BL-004](../status/backlog.md#blockers-18)) | Anyone who knows a registered phone number can read its login code and sign in as that user — **the single biggest exposure today** | Same — the endpoint is Development-gated in code; it disappears once the environment flips |
| Swagger served at `/swagger` | Full API surface exposed | Same |
| Seeders re-run on every boot; migrations auto-apply on boot | Safe today (idempotent), but not how a production release process should work | Run migrations as a one-shot command instead (`docker compose run --rm api dotnet Baya.Web.Api.dll migrate`) |
| gRPC reflection enabled; the demo `bookings/convert` payment-capture simulator is wired | Extra attack surface + a fake-payment code path reachable in a real deployment | Same — disappears with the environment flip |
**What must change**, per [DEPLOY.md "Going to Production"](../../DEPLOY.md#going-to-production-later):
1. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`.
2. Create `appsettings.Production.json` (it does not exist yet) with real `IdentitySettings:SecretKey` /
`Encryptkey``StartupSecretsGuard` rejects any value containing `not-for-production` outside Development,
so today's dev keys refuse to boot in Production by design. Keep `Seams:FieldEncryption` byte-identical to
the Development file (§1).
3. Run migrations as a one-shot instead of on boot.
4. Swap the OTP rail to `kavenegar` (§2) — the Telegram relay broadcasting every code to a fixed recipient
list stops being acceptable once someone outside that list can request a code.
---
## 4. Every Phase 4 blocker
"Blocker" means *the product is wrong or unusable* — that bar holds regardless of whether money is involved.
All 18 are pre-launch gate items; full detail and code traces are in
[backlog.md](../status/backlog.md#blockers-18).
| ID | One line | Rough effort |
| --- | --- | --- |
| BL-001 | Admin RBAC grants only the literal role `admin`; every seeded admin 403s everywhere | M — see [next-up.md](next-up.md) unit 3 |
| BL-002 | No seeded account holds the literal `admin` role, so even a fixed BL-001 is untestable out of the box | S — bundled with BL-001 |
| BL-003 | Committed live credentials, unrotated | See §1 |
| BL-004 | Dev-only OTP-read endpoint live on the production domain | See §3 |
| BL-005 | Card payment is a dead end everywhere — mock redirects to a non-existent host, no local webhook | M — see [next-up.md](next-up.md) unit 2 |
| BL-006 | Booking deadlines ship without a timezone; a 30-min window can render as ~4h and expire silently | S — see [next-up.md](next-up.md) unit 2 |
| BL-007 | No `bnpl` gateway row ever seeded; every BNPL call 400s | S — see [next-up.md](next-up.md) unit 4 |
| BL-008 | BNPL wizard's mock cross-imports the bookings-mock store; dead end for every real booking id | M — see [next-up.md](next-up.md) unit 4 |
| BL-009 | Refunds mock reads a retired store and would show a 10000%-scale refund on flip | M — see [next-up.md](next-up.md) unit 4 |
| BL-010 | Verification is 100% client-mocked; a server-verified nurse renders unverified everywhere | L — see [next-up.md](next-up.md) unit 1 |
| BL-011 | Patient/care records are 100% mocked; real DTO shapes would break on a naive flip | M |
| BL-012 | Nurse payouts: mock hides 4 working endpoints; no UI for the irreversible "process batch" step; "paid" status computed wrong | M — see [next-up.md](next-up.md) unit 5 |
| BL-013 | Partner center 100% mocked, zero real tenancy; 5 core routes don't exist server-side | L |
| BL-014 | Search results are index rows, not de-duplicated nurses; trust dossier mocked; unverified nurse profile page asserts "verified" | M |
| BL-015 | Editing an address silently nulls recipient name/phone/postal code on every save | S |
| BL-016 | A booking swept to `missed` never reaches a payable state | M |
| BL-017 | Reviews can never leave moderation on the live stack (sits behind BL-001; `AutoApproveClean` unset) | S once BL-001 lands |
| BL-018 | Zero option groups outside Development — every builder collapses to two steps, deployed | M |
---
## 5. Legal / tax items that are code-side
From [product/business/13-tax-invoicing-and-legal.md](../../product/business/13-tax-invoicing-and-legal.md),
confirmed the platform's weakest business area in [implemented.md](../status/implemented.md):
- **Terms & Privacy still ship placeholder legal copy behind a draft banner** — flagged for human/legal review
since ui-phase-3 and still unreviewed ([BL-097](../status/backlog.md#major-86)). Swapping in
counsel-reviewed copy (and removing the banner) is a pre-launch item, not a nice-to-have.
- **مودیان (e-invoice) integration is mocked past the point the business doc calls MVP.** The `invoices` model,
VAT split, and reference fields exist correctly (§ commission/VAT model below), but the Moadian
token-exchange refresh and signing certificate are unwired ([BL-236](../status/backlog.md#deferred-43)), and
seeded partner-center invoices carry null Moadian reference/PDF fields
([BL-200](../status/backlog.md#major-86)). These are deploy-time actions once real Moadian credentials
exist — but they don't exist yet, and مودیان readiness is explicitly named as MVP scope in the business doc.
- **The commission/VAT split itself is already correct and does not need code work**: platform commission is
15% of gross, VAT is 10% of the *commission only* — this became the single source of truth in
refinement-phase-3 ([decisions.md](../status/decisions.md)) and matches the business doc's nurse-as-
taxable-seller / platform-as-commission-seller model. The one open defect is a **display** bug, not a model
bug: the invoice screen computes a client-side row that's wrong by exactly the VAT amount instead of
rendering the server's real total ([BL-060](../status/backlog.md#major-86) — folded into
[next-up.md](next-up.md) unit 2).
- **`partner_centers` as merchant-of-record is the business doc's launch-critical legal vehicle** ("the fast,
legal go-to-market is to partner with already-licensed centers") — and it is 100% mocked with zero real
tenancy today ([BL-013](../status/backlog.md#blockers-18)). Until this is real, there is no functioning
legal invoice-issuer path for a booking routed through a partner center.
- **e-namad is a business/licensing prerequisite, not a code task, but it gates one**: per
[legal-landscape.md](../../product/research/legal-landscape.md), a monetized Iranian site needs e-namad to
obtain an online payment gateway at all — meaning the real ZarinPal flip in §2 cannot complete until the
launch entity holds e-namad, independent of anything in this codebase.
- **VAT-exempt-or-0% is a live legal question, not yet a code gap**: the business doc notes medical services'
own VAT treatment is unconfirmed in Iran and asks for the rate to stay config-driven so it can land either
way. Confirm with an Iranian tax advisor before launch, then confirm the rate is read from a
`platform_configs` row (the repo's config-is-rows convention) rather than a hardcoded constant — this
wasn't independently re-verified this phase and is worth a direct check before relying on it.
---
## Not in this file
Everything here is a **gate**, not a roadmap. What comes after the gate — the highest-leverage next units of
work, what's deliberately deferred and why, and what technical debt is accruing — is
[next-up.md](next-up.md), [deferred.md](deferred.md), and [tech-debt.md](tech-debt.md) respectively. Several
pre-launch items and next-up items overlap on purpose (e.g. BL-006, BL-060): the same fix both closes a
backlog item worth doing regardless of launch timing, and clears a launch gate.
+142
View File
@@ -0,0 +1,142 @@
# Tech debt — what it costs to not pay this yet
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> [documentation clean-up chain](../_plan/README.md).
Debt is different from [backlog.md](../status/backlog.md): a backlog item blocks a specific flow. A debt item
doesn't block anything today — it raises the cost of *everything that comes after it*. The phase brief named
six candidates to assess (items 2, 3, 4, 5, 7, 8 below); two of those (3, 4) turned out less severe than the
framing assumed once checked against the code, and this phase adds two more (1, 6) found while doing that
checking. Each entry below states what it costs now, what it costs at meaningfully more usage, and the
trigger that should cause it to get paid.
---
## 1. No contract-generation pipeline — client DTOs hand-drift from the server
**What it is:** `docs/integration/openapi/swagger.v1.json` is a regenerated, dated snapshot, but nothing turns
it into client types. `client/package.json` has no `openapi-typescript`/NSwag/orval step — every service
domain's TypeScript types are hand-written and hand-kept in sync with the real server DTOs.
| Now | At meaningfully more usage |
| --- | --- |
| The cost has already been paid twice, expensively: phase 2 found **24 phantom client-side endpoints** nobody had checked against swagger; phase 3 found **34 client seam operations that would 404 on a mock→real flip** and several DTO shape mismatches severe enough to *break*, not just show stale data (verification, refunds, payouts, patient-records, partner-center — see [implemented.md](../status/implemented.md)). Both discoveries took a dedicated full-session audit to surface. | Every future domain repeats this discovery cost, and nothing stops a **new** drift from being introduced tomorrow — there is no CI check comparing client types to the live contract. This compounds with domain count, not with traffic: the more domains behind mocks, the more of these audits eventually needed. |
**Trigger to pay it:** don't wait for the next silent break — the next 2-3 units in [next-up.md](next-up.md)
are exactly "flip a mocked domain to real." Wiring a generated-types step (even scoped to just the domains
being de-mocked next, not a big-bang rewrite) pays for itself on the very next flip.
**Rough size: M.**
## 2. Single-instance in-process scheduler
**What it is:** weekly payout-batch generation, and (once cron'd) the credential-expiry scan and EVV no-show
sweep, all run as in-process jobs with no distributed lock. Correct today because exactly one API instance
runs. Same root cause as [BL-220](deferred.md#scale-later-infrastructure) (Redis not deployed).
| Now | At meaningfully more usage |
| --- | --- |
| Zero cost — one instance, no race. | A rolling deploy that briefly runs two instances already risks a **duplicate weekly payout batch** — real money, generated twice. Horizontal scaling for any other reason (traffic, availability) can't happen without this being solved first; it's a hidden prerequisite baked into "just add another container." |
**Trigger to pay it:** the moment a second instance is even considered, for any reason — not just load.
**Rough size: M** (a distributed lock via Redis, per BL-220's own scoping).
## 3. Testing-convention asymmetry between the two projects
**Reassessed — this is not a raw-count gap.** Server test files (119, spanning `Baya.Test.Api` WebApplication-
Factory integration tests and `Baya.Test.Foundation` handler/unit tests) and client test files (125, Jest) are
close in count, and money-path server coverage is genuinely strong: Payments, Bookings, BNPL, Payouts, and
Refunds each have dedicated API-integration **and** handler-level test files. The real asymmetry is in
**enforcement**:
- [docs/rules/server/cqrs.md](../rules/server/cqrs.md) makes testing a mandatory step for every new
feature: "Add handler unit tests **and** at least one `WebApplicationFactory` integration test for the
area: happy path 200, unauthenticated 401, validation 400."
- Root [CLAUDE.md](../../CLAUDE.md)'s own client gate is reactive, not mandatory: "`npm run test:ci`
**if you touched a tested component**." New client code has no enforced testing bar at all.
| Now | At meaningfully more usage |
| --- | --- |
| 125 client tests exist, but — consistent with the flow atlas's own mock-vs-real accounting — coverage almost certainly concentrates on the domains that were real early and thins out on the ones still mocked. | As the mocked domains in [next-up.md](next-up.md) flip to real, the newly-real client code lands with no enforced test the way a newly-shipped server handler always does. Regressions there have no automated net; the flow atlas's manual, one-session verification (§4 below) is the only thing that has ever caught them. |
**Trigger to pay it:** natural to pair with each domain de-mock in [next-up.md](next-up.md) rather than run
as its own initiative — write the rule once, backfill tests as each domain is touched anyway.
**Rough size: S** to write the convention, **M per domain** to backfill.
## 4. No automated test crosses the client↔server boundary
**Reassessed — "absence of E2E tests over the money paths" overstates it.** The money paths have real
automated coverage at the handler and API-integration layers (§3). What's genuinely missing is anything that
drives a **real browser against a real running API** — no Playwright, no Cypress, nothing in
`client/package.json`'s scripts beyond Jest. That gap is exactly why phase 3's flow atlas had to boot the
whole stack and manually click/curl through all 23 flows to get a trustworthy status — a deliberate,
one-session, human/agent-driven substitute for automated E2E.
| Now | At meaningfully more usage |
| --- | --- |
| Absorbed as a one-time cost per audit — expensive (a full session), but infrequent. | This cost doesn't scale with traffic, it scales with **how often you need to trust a full-stack claim** — every future audit like phase 3's repeats the same manual walkthrough from scratch, because nothing keeps the previous one's findings mechanically re-checkable. |
**Trigger to pay it:** before the next full-stack audit is needed, or the first time a money-path regression
reaches production undetected — whichever comes first. **Rough size: M** — the server side is already well
tested, so this only needs to close the browser↔API gap: a handful of Playwright specs over the critical path
(login → search → book → pay → cancel), not a rewrite of anything existing.
## 5. Search without Elasticsearch
`SqlNurseSearch` over `nurse_search_index` is real, correct, and — per
[decisions.md](../status/decisions.md) — the deliberate MVP implementation, same item as
[BL-221](deferred.md#scale-later-infrastructure).
| Now | At meaningfully more usage |
| --- | --- |
| Fine at MVP data volume; no measured strain. | A wide denormalized SQL index degrades on filter/sort/paging combinations well before a purpose-built search engine would. |
**Trigger to pay it:** measured latency/throughput strain on `nurse_search_index` — not a date.
**Rough size: L.**
## 6. The ESLint unused-vars gate is a no-op
[BL-250](deferred.md#cleanup-of-convenience-and-data-integrity-hygiene): the config patches an export path
that doesn't carry the rule, so `@typescript-eslint/no-unused-vars` never actually runs — despite
`client/CLAUDE.md`'s own golden rule 11 claiming it does.
| Now | At meaningfully more usage |
| --- | --- |
| A safety net that looks present but isn't — dead code accumulates invisibly, with no signal to anyone that it's happening. | The longer this stays broken, the more has silently piled up by the time someone finally looks — this is a debt that compounds purely with time and commit count, independent of traffic or scale. |
**Trigger to pay it:** a dedicated infra task (this item's own stated trigger). **Rough size: S** — it's a
config-path bug, not a redesign.
## 7. Two remaining raw-state admin forms
[BL-217](../status/backlog.md#minor-115): `GrantRoleDialog` and `PreviewBatchDialog` are the only two
survivors of an otherwise-complete app-wide react-hook-form migration.
| Now | At meaningfully more usage |
| --- | --- |
| Trivial — two isolated components. | Still trivial. This is the one item on this list that genuinely doesn't get worse with scale; it's listed only because it's a rough edge for anyone reading the code expecting one form pattern everywhere. |
**Trigger to pay it:** whenever either dialog is next touched for an unrelated reason — not worth a dedicated
pass. **Rough size: S.**
## 8. Windows-generated client lockfile
Documented directly in [DEPLOY.md](../../DEPLOY.md#known-wrinkle-the-client-lockfile-is-windows-generated):
`client/package-lock.json`, generated on Windows, omits wasm32-only optional packages Linux's npm wants,
so a bare `npm ci` fails in the container build. Absorbed today by a `npm install --package-lock-only` step
baked into the client Dockerfile before `npm ci`.
| Now | At meaningfully more usage |
| --- | --- |
| Zero operational cost — the workaround runs on every image build without incident. | Still zero cost at scale; this is environment debt, not scale debt. The real risk is a future edit: anyone who "cleans up" the Dockerfile without knowing *why* that line is there reintroduces a build failure that only reproduces on Linux/CI, not on the Windows machine that likely made the edit. |
**Trigger to pay it:** DEPLOY.md already gives the exact fix — regenerate the lockfile on Linux once and
commit it, then delete the workaround line. **Rough size: S.** Cheapest item on this list to close permanently.
---
## What didn't make this list
`docs/_plan/README.md`'s own diagnosis (260+ markdown files, ~10 rule sources, contract drift) is the
documentation debt this entire phase chain exists to retire — phases 0-4 already paid most of it down, and
phase 6/7 finish the job. It isn't repeated here because it isn't *code* debt, and because tracking it twice
would just be two ledgers again, the exact problem this chain was created to end.
+1 -1
View File
@@ -286,7 +286,7 @@ handoff/report files for the full context.
| BL-242 | server | Automated clawback recovery beyond simple next-batch netting is not built. | handoff-after-backend-phase-13 | product pulls it | nurse-earnings-and-payouts | | BL-242 | server | Automated clawback recovery beyond simple next-batch netting is not built. | handoff-after-backend-phase-13 | product pulls it | nurse-earnings-and-payouts |
| BL-243 | server | Two-way (nurse-reviews-customer) double-blind reviews with timed reveal are not built. | handoff-after-backend-phase-14 | product pulls it | reviews | | BL-243 | server | Two-way (nurse-reviews-customer) double-blind reviews with timed reveal are not built. | handoff-after-backend-phase-14 | product pulls it | reviews |
| BL-244 | server | A first-class `incidents` entity is not built — manual suspension + support alerts stand in for now. | handoff-after-backend-phase-14 | product pulls it | admin-backoffice | | BL-244 | server | A first-class `incidents` entity is not built — manual suspension + support alerts stand in for now. | handoff-after-backend-phase-14 | product pulls it | admin-backoffice |
| BL-245 | server | `SuspendNurse`/`ResolveSupportAlert`/`FlagConcern` admin actions were never confirmed built in b15 — worth a direct re-check before relying on them. | backend-phase-14 follow-up | phase 5 verification pass | admin-backoffice | | BL-245 | server | **Verified 2026-08-02 (phase 5), resolving this row's own trigger:** `ResolveSupportAlert`, `AssignSupportAlert` (`SupportAlertsController.cs`), and nurse suspension (`AdminSuspendVerificationCommand`, feature folder `SuspendVerification`) are all real, built, and code-traced. Only `FlagConcern` — a softer flag-without-suspending action — is genuinely absent from the codebase; not separately filed as it has no UI or caller either. | backend-phase-14 follow-up, phase-5 roadmap verification | none — resolved; the residual `FlagConcern` gap is small enough to pick up opportunistically, not worth its own BL-### | admin-backoffice |
| BL-246 | server | Partner-center license verification is mocked to manual-approve at MVP rather than an automated eNamad/MoH check. | handoff-after-backend-phase-15 | product pulls it | partner-center | | BL-246 | server | Partner-center license verification is mocked to manual-approve at MVP rather than an automated eNamad/MoH check. | handoff-after-backend-phase-15 | product pulls it | partner-center |
| BL-247 | product | There is no telephony seam for emergencies — the emergency contact is an out-of-platform `tel:` link by deliberate design. | handoff-after-backend-phase-15 | none — permanent product decision | messaging-tickets | | BL-247 | product | There is no telephony seam for emergencies — the emergency contact is an out-of-platform `tel:` link by deliberate design. | handoff-after-backend-phase-15 | none — permanent product decision | messaging-tickets |
| BL-248 | client | SMS/push notification channels are not built — only in-app notifications are real. | handoff-after-refinement-phase-9 | notification UX demands out-of-app reach | notifications | | BL-248 | client | SMS/push notification channels are not built — only in-app notifications are real. | handoff-after-refinement-phase-9 | notification UX demands out-of-app reach | notifications |
+6
View File
@@ -129,3 +129,9 @@ never just cites the flag.
`dev/shared-working-context/backend/handoff/`, every item in [backlog.md](backlog.md)'s Deferred section `dev/shared-working-context/backend/handoff/`, every item in [backlog.md](backlog.md)'s Deferred section
carries the condition that should cause it to be picked up (a specific product decision, a scale threshold, carries the condition that should cause it to be picked up (a specific product decision, a scale threshold,
a second integration) rather than a scheduled date. Phase 5 owns turning these into a sequenced roadmap. a second integration) rather than a scheduled date. Phase 5 owns turning these into a sequenced roadmap.
**BL-245's own trigger ("phase 5 verification pass") was executed during phase 5.** `ResolveSupportAlert`,
`AssignSupportAlert`, and nurse suspension (`AdminSuspendVerificationCommand`) were all confirmed real and
code-traced; only `FlagConcern` is genuinely unbuilt. Recorded here rather than left as an open question in
[roadmap/deferred.md](../roadmap/deferred.md) — a small, direct illustration of "verify, don't copy" applied
one phase later than the item that requested it.