cleanup phase 5
This commit is contained in:
@@ -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 | | |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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).
|
||||||
+79
-18
@@ -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 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 |
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user