From e2db97392a5006520911e01fa14cd26bc1405757 Mon Sep 17 00:00:00 2001 From: hamid Date: Sun, 2 Aug 2026 18:33:43 +0330 Subject: [PATCH] cleanup phase 5 --- docs/_plan/README.md | 2 +- docs/_plan/phase-5-roadmap.md | 63 +++++++++- docs/roadmap/deferred.md | 140 +++++++++++++++++++++++ docs/roadmap/index.md | 97 +++++++++++++--- docs/roadmap/next-up.md | 210 ++++++++++++++++++++++++++++++++++ docs/roadmap/pre-launch.md | 177 ++++++++++++++++++++++++++++ docs/roadmap/tech-debt.md | 142 +++++++++++++++++++++++ docs/status/backlog.md | 2 +- docs/status/decisions.md | 6 + 9 files changed, 818 insertions(+), 21 deletions(-) create mode 100644 docs/roadmap/deferred.md create mode 100644 docs/roadmap/next-up.md create mode 100644 docs/roadmap/pre-launch.md create mode 100644 docs/roadmap/tech-debt.md diff --git a/docs/_plan/README.md b/docs/_plan/README.md index 4fefdcf..0b74182 100644 --- a/docs/_plan/README.md +++ b/docs/_plan/README.md @@ -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 | | 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 | -| 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 | | | | 7 Skills & guardrails | not started | | | diff --git a/docs/_plan/phase-5-roadmap.md b/docs/_plan/phase-5-roadmap.md index 44647bd..032a3ab 100644 --- a/docs/_plan/phase-5-roadmap.md +++ b/docs/_plan/phase-5-roadmap.md @@ -118,4 +118,65 @@ that piece and not another. ## 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. diff --git a/docs/roadmap/deferred.md b/docs/roadmap/deferred.md new file mode 100644 index 0000000..1d910a1 --- /dev/null +++ b/docs/roadmap/deferred.md @@ -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-220–BL-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). diff --git a/docs/roadmap/index.md b/docs/roadmap/index.md index e69ef54..7fa40ce 100644 --- a/docs/roadmap/index.md +++ b/docs/roadmap/index.md @@ -1,27 +1,88 @@ # Roadmap — where it goes next -> **Populated by phase 5 — not yet written.** Phase 5 depends on phase 4: you cannot sequence work -> that has not been reconciled into one backlog. +> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the +> [documentation clean-up chain](../_plan/README.md). -A **proposal**, not a commitment. The ordering is reasoned and can be overruled; what is not negotiable -is that every item traces to a `BL-###` in [status/backlog.md](../status/index.md), so nothing is -scheduled that was not first triaged. +A **proposal**, not a commitment. The ordering is reasoned and can be overruled; what is not negotiable is +that every item traces to a `BL-###` in [status/backlog.md](../status/backlog.md), so nothing here was +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 | -| --- | --- | -| `next-up.md` | The next 3–5 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 | +## The four documents -## 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, -documented in [root CLAUDE.md §6](../../CLAUDE.md) and [DEPLOY.md](../../DEPLOY.md). Before onboarding -real users those values must be rotated and the secret half moved out of git. +documented in [root CLAUDE.md §6](../../CLAUDE.md) and [DEPLOY.md](../../DEPLOY.md). Before onboarding real +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 -every encrypted column and derive the `PhoneHash` that every login looks up. Rotating them locks every -account out. +One value is load-bearing and must **never** be rotated in place: `Seams:FieldEncryption:Key` and `:HashKey` +decrypt every encrypted column and derive the `PhoneHash` every login looks up. Changing them without a +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. diff --git a/docs/roadmap/next-up.md b/docs/roadmap/next-up.md new file mode 100644 index 0000000..1155437 --- /dev/null +++ b/docs/roadmap/next-up.md @@ -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. diff --git a/docs/roadmap/pre-launch.md b/docs/roadmap/pre-launch.md new file mode 100644 index 0000000..24f1223 --- /dev/null +++ b/docs/roadmap/pre-launch.md @@ -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. diff --git a/docs/roadmap/tech-debt.md b/docs/roadmap/tech-debt.md new file mode 100644 index 0000000..2b7becd --- /dev/null +++ b/docs/roadmap/tech-debt.md @@ -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. diff --git a/docs/status/backlog.md b/docs/status/backlog.md index c54acf7..c46330b 100644 --- a/docs/status/backlog.md +++ b/docs/status/backlog.md @@ -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-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-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-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 | diff --git a/docs/status/decisions.md b/docs/status/decisions.md index 438e606..79b2941 100644 --- a/docs/status/decisions.md +++ b/docs/status/decisions.md @@ -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 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. + +**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.