# Backlog — every open item, triaged

> Last verified: 2026-08-02 against commit `51e86a1`.

Reconciled from five ledgers — [hardening/issues.md](../../archive/post-phase/hardening/issues.md) (18 items),
[for-backend.md](../../archive/build-chain/working-context/frontend/requests/for-backend.md) (67 REQs), 53
report "Follow-ups" sections, 22 backend handoff files, two manual-testing iterations — plus
[product/notes/open-questions.md](../../product/notes/open-questions.md), root [CLAUDE.md](../../CLAUDE.md)
§6, phase 2's drift list, and phase 3's **283 flow gaps** (the primary, freshest, code-verified input; see
[docs/flows/index.md](../flows/index.md)). ~700 raw candidate rows were harvested; this file is the deduped,
triaged result. Old ids stay greppable in their source files — nothing was rewritten.

**Area**: `client` · `server` · `contract` · `ops` · `product` · `docs`. **Sev**: `blocker` (product is wrong
or unusable) · `major` · `minor` · `deferred`. **Status**: `open` · `in-progress` · `blocked (on what)` ·
`deferred (trigger)`. **Blocks**: the [docs/flows/](../flows/index.md) journey(s) it degrades.

Closed items (with what closed them) are in [backlog-closed.md](backlog-closed.md). The distilled decision
log is in [decisions.md](decisions.md). The business-area overlay is in [implemented.md](implemented.md).

---

## Blockers (18)

| ID | Area | Item | Origin | Status | Blocks |
|----|------|------|--------|--------|--------|
| BL-001 | server | Admin RBAC is structurally dead: `DynamicPermissionService.CanAccess` grants only the literal role `admin`; every seeded admin holds `super_admin`/`finance` and 403s on all `DynamicPermission`-gated controllers. No code path ever writes a `DynamicPermission` claim. | H-04, H-05, admin-backoffice gap-1/3/12, bnpl gap-7, cancellation-and-refunds gap-9, messaging-tickets gap-1/3, nurse-verification gap-3, nurse-earnings-and-payouts gap-7, partner-center gap-8, reviews gap-1/2, booking-request gap-3, booking-lifecycle-evv gap-3/5, nurse-catalog-and-pricing gap-2 | open | admin-backoffice, bnpl-installments, cancellation-and-refunds, messaging-tickets, nurse-verification, nurse-earnings-and-payouts, partner-center, reviews, booking-request, booking-lifecycle-evv, nurse-catalog-and-pricing |
| BL-002 | ops | No seeded account holds the literal `admin` role (`Seed:AdminUsername`/`Password` unset, `SeedDataBase.cs` returns early) — even a fixed BL-001 is untestable out of the box. | admin-backoffice gap-2 | open | admin-backoffice |
| BL-003 | ops | Committed live credentials (DB `sa`, JWE/field-encryption key halves, Kavenegar, Neshan, Finnotech, Telegram bot token) must be rotated and moved out of git before onboarding real users. | CLAUDE.md §6, refinement-phase-0 follow-up 3 | open | — (pre-launch) |
| BL-004 | ops | `GET /api/v1/dev/last_otp/{phone}` is anonymous and Development-only by design, but the deployment runs as Development — live on `api.balinyaar.ir`, lets anyone who knows a phone read its login code. | auth-login-otp gap-9, public-front-door gap-10 | open | auth-login-otp, public-front-door |
| BL-005 | server | Card payment is a dead end everywhere: `MockPaymentProvider` redirects to a non-existent host, and nothing fires the PSP webhook locally, so no new card payment can ever reach `ConfirmPaymentAndPostLedger`. | checkout-and-payment gap-1/2 | open | checkout-and-payment |
| BL-006 | contract | Booking deadline timestamps ship without a timezone (`DateTime` not `DateTimeOffset`); the client reads the offset-less value as local time — in Tehran the 30-minute payment window renders ~4h and expires while the timer still shows time left. | booking-request gap-1 | open | booking-request, checkout-and-payment |
| BL-007 | ops | No `bnpl` payment-gateway row is ever seeded — every BNPL initiate/eligibility call 400s with "No active BNPL gateway is configured" on the live server. | bnpl gap-1 | open | bnpl-installments |
| BL-008 | client | The BNPL wizard is a dead end for every real booking-request id: its mock cross-imports the bookings-mock store (seeded ids 1-2 only), bypassing the real-data flip entirely. | H-07, bnpl gap-2/3 | open | bnpl-installments |
| BL-009 | client | Refunds mock reads the retired bookings-mock store (ids 5001-5005 only), so any real booking id 404s inside the cancel-policy preview; flipping the flag today would also render a 10000% refund (percent-scale bug on an already-0-100 field). | H-06, cancellation-and-refunds gap-1/2/4 | open | cancellation-and-refunds |
| BL-010 | client | Verification is 100% client-mocked under one flag covering nurse + public + admin; the mock resets to empty on every reload, so even a server-verified nurse renders unverified everywhere the app reads it, hiding the real search-visibility gate and "go live" CTA. `submitCredentialDetails` is also a no-op though the real endpoint matches field-for-field. | H-08, nurse-verification gap-1/2/4, onboarding-nurse gap-1/2/3 | open | nurse-verification, onboarding-nurse, search-and-discovery, public-front-door |
| BL-011 | client | Patient/care records are 100% mocked on both the family record and nurse visit-note panel, and the real DTO shapes would break a naive flip: medication/routine fields mismatch server field-for-field, and care-plan ids are `string` client-side vs `long` server-side. | care-circle-patients gap-1..4, patient-care-records gap-1..3, H-17 | open | care-circle-patients, patient-care-records, booking-lifecycle-evv |
| BL-012 | server | Nurse earnings/payouts: mock hides four working endpoints; no `process` operation exists anywhere in the client (the irreversible payout step has no UI); `DeriveEarningsState` marks a booking "paid" whenever it merely has a payout *link* regardless of that payout's actual status; the four earnings buckets don't reconcile against the ledger. | H-09, nurse-earnings-and-payouts gap-1/4/5/9/10/11 | open | nurse-earnings-and-payouts |
| BL-013 | client | Partner center is 100% mocked with zero tenancy (any signed-in caller resolves to center id 1), and 5 core routes (`centers/me`, `/me/nurses`, `/me/bookings`, `/me/bookings/{id}`, `/me/settlement`) don't exist server-side; a center owner has no legal read of its own issued invoices. | partner-center gap-1/2/3/9/10, REQ-032, REQ-033, REQ-064 | open | partner-center |
| BL-014 | server | Search results are index rows, not nurses — one nurse with 3 variants × 3 areas shows as "9 پرستار" with no de-dup; the trust dossier on results is mocked, and the by-id profile page asserts "verified" unconditionally, even for an `in_review` nurse. | search-and-discovery gap-1/3/4 | open | search-and-discovery, public-front-door |
| BL-015 | client | Editing an address through the UI silently destroys data: the form never collects `postalCode`/`recipientName`/`recipientPhone`, and the update handler nulls them unconditionally on every save. | addresses-and-map gap-4 | open | addresses-and-map |
| BL-016 | server | A booking whose remaining sessions get swept to `missed` never reaches a payable state — the `allSettled → Completed` re-check only lives inside the checkout path, so already-checked-out sessions can never enter a payout batch. The "today" session feed also applies no date filter and lists a nurse's whole history. | booking-lifecycle-evv gap-1/2 | open | booking-lifecycle-evv, nurse-earnings-and-payouts |
| BL-017 | server | Reviews can never leave moderation on the live stack: the queue and status-PATCH sit behind BL-001, and `AutoApproveClean` is absent from config — a real review never reaches a nurse's profile except via the seeder. | reviews gap-1/2/3 | open | reviews |
| BL-018 | server | A production database has zero option groups outside Development — the one seeded «نوع شیفت» group only exists because the demo seeder is Dev-only, so every builder collapses to two steps and every variant in a category duplicates every other, deployed. | nurse-catalog-and-pricing gap-3 | open | nurse-catalog-and-pricing |

## Major (86)

| ID | Area | Item | Origin | Status | Blocks |
|----|------|------|--------|--------|--------|
| BL-019 | client | Customer without an emergency contact cannot save name or language — the profile validator requires it non-empty but the UI has no field-level pre-check, and the resulting 400 is completely silent (no `onError`). | account-and-settings gap-1/2 | open | account-and-settings |
| BL-020 | client | Partner settings hub shows fabricated identity — `USE_PARTNER_MOCK` resolves the same center for any caller, zero real tenancy on the screen. | account-and-settings gap-6 | open | account-and-settings, partner-center |
| BL-021 | client | Sign-out confirmation is inconsistent — customer hub gates it behind a confirm dialog; nurse/admin/partner fire on the first tap and end every session on the account. | account-and-settings gap-7 | open | account-and-settings |
| BL-022 | client | A customer cannot set an avatar — the real endpoint exists but the client's `uploadAvatar` only targets the nurse route. | account-and-settings gap-8 | open | account-and-settings |
| BL-023 | product | `preferredLanguage` is stored and never consumed anywhere — the UI locale comes only from the URL prefix, so the زبان sheet has two controls that do different things. | account-and-settings gap-3 | open | account-and-settings |
| BL-024 | server | Nurse avatars never load anywhere in the app — `LocalDiskObjectStorage.GetUrl` returns a `file://` URI a browser cannot fetch. | account-and-settings gap-4, onboarding-nurse gap-4, search-and-discovery gap-6, public-front-door gap-2 | open | account-and-settings, onboarding-nurse, search-and-discovery, public-front-door, booking-request |
| BL-025 | ops | `NEXT_PUBLIC_NESHAN_KEY` is unset in every environment, so the real map (tiles, address search, locate-me) never renders anywhere — every environment gets the keyless grid stand-in, and the response shapes are unverified against the live Neshan API. | addresses-and-map gap-1/2 | open | addresses-and-map |
| BL-026 | client | `CITY_CENTROIDS` is keyed on mock-seed city ids; only Tehran matches the real seeded cities, so the map picker opens ~400km away for every other city. | addresses-and-map gap-3 | open | addresses-and-map |
| BL-027 | client | Recipient name/phone/postal-code are never displayed anywhere on an address card either, even where the (broken, BL-015) form would collect them. | addresses-and-map gap-5 | open | addresses-and-map |
| BL-028 | contract | Admin list pagination silently breaks on flip: `admin/apis/clientApi.ts` sends `page_size`; controllers declare `PageSize`. Binding is case- but not separator-insensitive, so every admin list falls back to the server default page size. | admin-backoffice gap-6, search-and-discovery gap-13 | open | admin-backoffice, search-and-discovery |
| BL-029 | server | `/fa/admin/roles` and `/fa/admin/users` have no server — `admin_roles/*` and `admin_users/*` are phantom; REQ-061 (admin user directory) was never given a ledger header despite ten live client files citing it. | admin-backoffice gap-7/8, REQ-031, REQ-061 | open | admin-backoffice |
| BL-030 | client | `admin_cancellation_policies/list|upsert` are unwired — no screen edits the cancellation tiers; they are effectively read-only in production. | admin-backoffice gap-11 | open | admin-backoffice, cancellation-and-refunds |
| BL-031 | client | Reuse-detection cannot tell theft from two tabs: the single-flight refresh guard is per-tab module state, so two tabs 401ing at once race and the loser's classified-as-reuse kills every session with no explanation. `useSelectRole`'s own rotation bypasses the guard the same way. | auth-login-otp gap-2/3 | open | auth-login-otp |
| BL-032 | server | Session revocation does not kill access tokens — `RefreshTokenCommandHandler` revokes sessions but never rotates the security stamp, so "logged out everywhere" is up to 60 minutes late. | auth-login-otp gap-4 | open | auth-login-otp |
| BL-033 | client | The access cookie expires 45 minutes before the token does (900s vs 60min), so 15 idle minutes bounces any full page load to `/login` even though the refresh cookie could have recovered the session. | auth-login-otp gap-5 | open | auth-login-otp |
| BL-034 | client | A `?next=` pointing at `/partner/...` never survives login — `appRoleForPath` has no partner branch, and `/me` carries no signal that the caller administers a partner center. | auth-login-otp gap-11, partner-center gap-4, REQ-038 | open | auth-login-otp, partner-center |
| BL-035 | client | `useLogout` always signs out every device — an absent `refreshToken` means "everywhere"; no UI offers a single-device choice. | auth-login-otp gap-12 | open | auth-login-otp |
| BL-036 | client | `resolveRoleDestination` checks admin before customer/nurse, so a user holding both an admin sub-role and `customer` can never reach the family app from login. | auth-login-otp gap-13 | open | auth-login-otp |
| BL-037 | ops | `POST /auth/request_otp` 500s on a fresh clone — the committed SMS provider is `telegram` with nothing listening; login is unreachable until booted with the mock provider. | auth-login-otp gap-1 | open | auth-login-otp |
| BL-038 | client | Query cache is never cleared on logout/login — only `authKeys` are invalidated, so a device that signs out of one account and into another can render stale, cross-account data from the previous session's cache. | H-11 | open | auth-login-otp, account-and-settings |
| BL-039 | server | `BookingRoles.Admin` over-grants: Support/Moderation roles get clinical care-instruction reads and financial nurse-payable-balance reads, wider than the "admin/finance" comment claims. | H-05 | open | booking-lifecycle-evv, nurse-earnings-and-payouts |
| BL-040 | client | The «پاسخ‌داده» nurse-inbox tab is unpaged — the API filters one status at a time with no group filter, so `nurse/requests` fires three page-1 queries and concatenates; a nurse with >20 answered requests silently loses rows. | booking-request gap-4, REQ-050 (residue) | open | booking-request |
| BL-041 | ops | Nothing seeded is actionable in the booking-request flow — no `pending`/`accepted` request survives the demo seeder's epoch-anchored aging, so the nurse inbox, accept/reject and payment-window handoff are all unwalkable without creating a fresh request. | booking-request gap-2 | open | booking-request |
| BL-042 | server | `nurseAvatarUrl` on a booking request is served as a raw local-disk path — same root cause as BL-024, called out because checkout renders it directly. | booking-request gap-11 | open | checkout-and-payment |
| BL-043 | client | `NEXT_PUBLIC_EVV_MOCK_GPS` now defaults to off (derived from the now-real bookings flag), so a tester away from the seeded Tehran address fires a real GPS mismatch — and a support alert — on every check-in; the doc-comment still describes the old default. | booking-lifecycle-evv gap-6 | open | booking-lifecycle-evv |
| BL-044 | server | The nurse never sees the address on a confirmed booking — `BookingMapper.ToDetailDto` nulls the address snapshot for the nurse role even though the server itself reads it to compute the EVV match. | booking-lifecycle-evv gap-7, REQ-051 | open | booking-lifecycle-evv |
| BL-045 | client | `POST bookings/submit_care_instructions/{id}` is unwired — no customer form exists anywhere; on a booking a tester creates, the nurse's care-instructions card is empty. | booking-lifecycle-evv gap-10 | open | booking-lifecycle-evv |
| BL-046 | client | The nurse booking detail is half real: the bookings/EVV half is server truth, but the mounted visit-notes panel (task checklist, continuity history, note-save) is entirely `patientRecords`-mocked — same root cause as BL-011. | booking-lifecycle-evv gap-12 | open | booking-lifecycle-evv |
| BL-047 | client | H-06's exact mocked-cross-import defect also shows up as: `CHANNEL_BY_BOOKING` pins the BNPL-revert refund channel to one fixture id, so every real booking silently demos as a card refund. | cancellation-and-refunds gap-3 (folded severity note) | open | cancellation-and-refunds |
| BL-048 | contract | The client discards six fields the server now serves on refund status (`platformFeeRefundedIrr`, `nursePayoutRefundedIrr`, `refundPercentageApplied`, `cancellationPolicyCode`, `createdAt`, `completedAt`) — the fee-split transparency section can never render even once BL-009 is fixed. | cancellation-and-refunds gap-5 | open | cancellation-and-refunds |
| BL-049 | contract | Cancellation enum drift on three fields (`CancellationPolicyCode`, `CancellationLeadTime`, `appliesTo`) between client and the seeded/wire values — every one drives an i18n lookup that will miss on flip. | cancellation-and-refunds gap-6 | open | cancellation-and-refunds |
| BL-050 | server | `POST admin_refunds` creates and executes in one call — no preview/approve/reject route exists, so the admin `RefundPanel`'s three-step console has no real backend. | cancellation-and-refunds gap-10, REQ-035 | deferred (trigger: admin refund console prioritized) | cancellation-and-refunds |
| BL-051 | client | No admin console exists for refund/clawback settlement — `confirm_settlement`, `mark_failed`, `write_off` all have server handlers but zero client screens. | cancellation-and-refunds gap-12 | open | cancellation-and-refunds |
| BL-052 | server | `expectedCustomerRefundEta` is hardcoded null in the cancellation preview — the BNPL 10-business-day window is invisible before the customer confirms, exactly where it matters most. | cancellation-and-refunds gap-14 | open | cancellation-and-refunds |
| BL-053 | ops | `customer_no_show` has no seeded cancellation-policy row — the product doc's "up to 100% charge" tier does not exist; a customer no-show falls through to the 50%-back tier. | cancellation-and-refunds gap-15 | open | cancellation-and-refunds |
| BL-054 | server | No credit note / invoice reversal on refund — a refunded booking's VAT-bearing commission invoice stays as originally issued. | cancellation-and-refunds gap-16 | open | cancellation-and-refunds |
| BL-055 | server | Seeded visit-note `taskResults` labels are lost — the handler deserializes with case-sensitive options against camelCase seeded JSON; any externally-written JSON in that column silently degrades to blank labels. | care-circle-patients gap-5, patient-care-records gap-7 | open | care-circle-patients, patient-care-records |
| BL-056 | client | Editing a patient destroys their birth date — the form always submits `ageToBirthDate(age)` = `YYYY-01-01`, overwriting the real recorded date on every save even with no change. | care-circle-patients gap-10 | open | care-circle-patients |
| BL-057 | server | Tenancy leak on patient records: a foreign patient returns 403, not 404, across all three handlers — confirms the patient exists to an unauthorized caller, violating the repo's 404-not-403 invariant. | patient-care-records gap-8 | open | care-circle-patients, patient-care-records |
| BL-058 | contract | A verified nurse is shown as unverified on the checkout screen — the checkout summary DTO declares `nurseVerified` but the server never constructs it. | checkout-and-payment gap-3, REQ-046 | open | checkout-and-payment |
| BL-059 | client | Payment confirmation cannot deep-link to the booking — the server does serve `bookingId` but the client types it away and hardcodes null; the receipt also always hides the tracking code and paid-at. | checkout-and-payment gap-5, H-10, REQ-046 | open | checkout-and-payment |
| BL-060 | client | The invoice screen derives a money row client-side (`gross − commission − vat`) that is wrong by exactly the VAT amount, and never shows the server's real `totalIrr` — violates the "client never computes money" rule. | checkout-and-payment gap-7 | open | checkout-and-payment |
| BL-061 | server | Customer payment history is a live 404 — `bookings/payment_history` doesn't exist; the wallet «پرداخت‌ها» tab is permanently empty for a card-paying customer. | checkout-and-payment gap-8, REQ-047 | open | checkout-and-payment |
| BL-062 | server | The escrow ledger has no read surface at all — a customer, admin or tester has no way to see the balanced capture group the flow's core money invariant depends on. | checkout-and-payment gap-10 | open | checkout-and-payment |
| BL-063 | client | `getUnreadTotal` is a hardcoded `null` on the real path — the chrome support badge can never light up. | messaging-tickets gap-4, account-and-settings gap-10, REQ-059 | open | messaging-tickets, account-and-settings |
| BL-064 | client | `mapSummary` hardcodes `lastMessagePreview`/`lastAuthorRole` to null on the real path — the ticket inbox card shows no message preview. | messaging-tickets gap-5, REQ-059 | open | messaging-tickets |
| BL-065 | client | `POST /tickets/emergency` has zero client callers — the nurse's emergency banner only dials `tel:`; the "then opens a ticket" half of the business playbook is manual. | messaging-tickets gap-6 | open | messaging-tickets |
| BL-066 | contract | 11 of 14 server notification types are unknown to the client's parser — only 3 deep-link; the nurse's most important notification (`booking_confirmed_nurse`) and the entire `booking_request_*` lifecycle are non-navigable. | notifications gap-1/2/3 | open | notifications, booking-request |
| BL-067 | client | The client handles 10 notification types the server never emits, while the server's real payout-paid/payout-failed events have no client branch at all — a nurse is never told a payout paid or failed. | notifications gap-6 | open | notifications, nurse-earnings-and-payouts |
| BL-068 | server | Every notification title/body is an English literal rendered verbatim into the Persian RTL feed — no i18n path for notification content, only for chrome. | notifications gap-7 | open | notifications |
| BL-069 | client | No catalogue-authoring UI exists at all — all seven `admin_catalog/*` routes have zero client callers; an admin cannot add a pricing dimension without SQL, compounding BL-018. | nurse-catalog-and-pricing gap-1 | open | nurse-catalog-and-pricing |
| BL-070 | server | ZWNJ is stripped from every stored Persian string via a global `FixPersianChars` normalizer, contradicting the repo's "with a ZWNJ, always" naming rule. | nurse-catalog-and-pricing gap-4 | open | nurse-catalog-and-pricing |
| BL-071 | server | A nurse's price edit is not shielded from an in-flight booking request — the conversion reads the live variant price, not a request-time snapshot, so editing price inside the 30-minute payment window changes what the customer pays. | nurse-catalog-and-pricing gap-5 | open | nurse-catalog-and-pricing, booking-request |
| BL-072 | client | The «همراهی و مراقبت روزمره» category is data-only per its own seed comment, but the builder offers it like any real pricing path. | nurse-catalog-and-pricing gap-8 | open | nurse-catalog-and-pricing |
| BL-073 | client | `toHistoryItem` hardcodes `failureReason: null` though the live wire carries it — a failed payout loses its reason in the nurse's history on flip. | nurse-earnings-and-payouts gap-3 | open | nurse-earnings-and-payouts |
| BL-074 | client | `previewPayoutBatch` sums net amounts and fabricates a processing date client-side — the client computing money and a payout date, violating the "client never computes money" rule. | nurse-earnings-and-payouts gap-6 | open | nurse-earnings-and-payouts |
| BL-075 | contract | `recordTransferReference` targets a route that doesn't exist — the batch-detail reconcile field 404s on flip. | nurse-earnings-and-payouts gap-12, REQ-036 | deferred (trigger: payout reconciliation console prioritized) | nurse-earnings-and-payouts |
| BL-076 | client | `POST admin_payouts/{id}/mark_failed` exists server-side but has no client op — a reconciled bank rejection cannot be recorded from the console. | nurse-earnings-and-payouts gap-14 | open | nurse-earnings-and-payouts |
| BL-077 | server | Whole-city + a specific district in the same city are both accepted for one nurse, and search then returns her twice with no de-dup on `(nurseId, variantId)`. | nurse-service-areas gap-1 | open | nurse-service-areas, search-and-discovery |
| BL-078 | client | The coverage screen has no error state — `isError` is dropped, so a failed list load renders the "no area registered" empty state, telling a nurse with real coverage she is invisible in search. | nurse-service-areas gap-3 | open | nurse-service-areas |
| BL-079 | contract | No edit and no deactivate for a service area — changing a district means remove-then-add, which silently drops the nurse from search between the two calls. | nurse-service-areas gap-5 | open | nurse-service-areas |
| BL-080 | client | `nurse/verification/page.tsx` unconditionally imports the verification mock module — the one production seam breach in the client; the mock ships in every build regardless of the render gate. | nurse-verification gap-4 | open | nurse-verification |
| BL-081 | client | `foldQueueRows` folds the per-step admin verification queue to per-nurse items but leaves the step-progress counts at zero and the pager count wrong — a nurse's steps can straddle a page boundary. | nurse-verification gap-6, REQ-062 | open | nurse-verification |
| BL-082 | contract | `approveVerification`/`rejectVerification`/`getDocumentSignedUrl` target routes that don't exist — the admin case page's visible approve/reject CTAs would 404 the moment BL-001 is fixed. | nurse-verification gap-7, REQ-034 | deferred (trigger: verification-admin console prioritized) | nurse-verification |
| BL-083 | client | No admin UI exists for `suspend`/`scan_expiring` or the verification step-type catalog — the "data-driven catalog" design rule has no admin surface. | nurse-verification gap-8 | open | nurse-verification |
| BL-084 | client | Admin verification case URLs are mistyped — the route folder is `[nurseId]` but the value passed is a `nurseVerificationId`; typing a nurse id into the URL opens the wrong case. | nurse-verification gap-11 | open | nurse-verification |
| BL-085 | product | Onboarding never creates a `customer_profile` — it creates a patient and stops; nothing blocks a customer from booking without ever setting an emergency contact. | onboarding-customer gap-1 | open | onboarding-customer |
| BL-086 | contract | A customer's own `gender` is unsettable through the app — no client call and no command field carries it; a fresh account's `/me.gender` stays null forever. | onboarding-customer gap-3 | open | onboarding-customer |
| BL-087 | client | The customer profile upsert has no PATCH semantics — every sheet save rewrites the whole profile including name, so a stale form on one device can silently overwrite a name changed on another. | onboarding-customer gap-5 | open | onboarding-customer, account-and-settings |
| BL-088 | client | `/fa/select-role` is reachable by any authenticated user with no guard, letting a customer permanently self-grant the `nurse` role by typing a URL, with no confirmation step for an irreversible grant. | onboarding-customer gap-7 | open | onboarding-customer |
| BL-089 | client | `useSelectRole` swallows a failed post-select token rotation — the role persists server-side but the client keeps a stale token until the next silent refresh happens to fire, so a role-gated call in between 403s. | onboarding-customer gap-9 | open | onboarding-customer |
| BL-090 | client | The duplicate-IBAN error message never reaches the nurse — the bank page discards the server's field error and shows a generic "registration failed" toast. | onboarding-nurse gap-7 | open | onboarding-nurse |
| BL-091 | server | Bank-ownership inquiry ignores the national id entirely — a nurse who never did identity KYC (`national_id = NULL`) still gets `matchedNationalId = true`, so the payout gate opens on a claim nothing actually checked. | onboarding-nurse gap-8 | open | onboarding-nurse, nurse-earnings-and-payouts |
| BL-092 | client | `verifyOwnership` is a dead seam op — no hook calls it, so a `mismatch` bank account has no re-inquiry affordance; the UI only offers "re-enter the IBAN". | onboarding-nurse gap-9 | open | onboarding-nurse |
| BL-093 | contract | Shape mismatch blocks the partner portal even once mocked data is off: the server serves one capped/inline dashboard aggregate; the portal needs paginated `/me`-scoped splits it cannot get from that shape. | partner-center gap-5 | open | partner-center |
| BL-094 | client | `GET /centers/{id}/dashboard` — the one portal endpoint that *is* real — needs a center id the portal has no way to discover, so it goes unused. | partner-center gap-6 | open | partner-center |
| BL-095 | server | Structured `taskResults` are discarded in both directions on the real patient-records path — accepted on write and returned on read by the server, but the client hardcodes them away; the "wire has no structured field" client comments are simply wrong. | patient-care-records gap-6 | open | patient-care-records |
| BL-096 | product | Tier (c) of the public front door (guest search + public nurse profiles) is unbuilt — REQ-066/067 need a backend phase plus an explicit privacy sign-off before any guest-facing search/profile screen is built. | public-front-door gap-6, ui-phase-13 follow-up | deferred (trigger: privacy sign-off + REQ-066/067 delivered) | public-front-door, search-and-discovery |
| BL-097 | product | `/terms` and `/privacy` ship placeholder legal copy behind a draft banner, flagged for human/legal review since ui-phase-3 and still unreviewed — a real pre-launch item. | public-front-door gap-5, ui-phase-3 follow-up 1 | open | public-front-door |
| BL-098 | server | `GET /api/v1/nurses/{id}/profile` returns 200 anonymously for an unverified (`in_review`) nurse — the persona that "must never appear in search" is reachable and enumerable by id, even though search itself correctly hides her. | public-front-door gap-3 | open | public-front-door, search-and-discovery |
| BL-099 | client | The nurse cannot see their own reviews anywhere in the app — the only signal is a notification the (BL-001-blocked) moderation handler dispatches. | reviews gap-8 | open | reviews |
| BL-100 | client | The client hardcodes the five review tag codes instead of reading the server's tag master — a newly seeded tag is invisible to the UI and an unseeded code fails submit outright. | reviews gap-7 | open | reviews |
| BL-101 | server | `ReviewModerationStatus.Rejected` is unreachable end to end — the submit-time banned-word path maps to `Hidden`, not `Rejected`, and the only other producer is the BL-001-blocked admin PATCH; the client still renders a `rejected` chip nobody can ever trigger. | reviews gap-4 | open | reviews |
| BL-102 | server | Review eligibility distinguishes "booking not found" from "not your booking" with a 200, letting any authenticated caller probe whether an arbitrary booking id exists — violates the repo's 404-not-403 tenancy invariant. | reviews gap-9 | open | reviews |
| BL-103 | product | No free-text search exists — a customer who knows a nurse's name by name cannot find her; category/city/gender/price filters only. | search-and-discovery gap-10, REQ-041 | open | search-and-discovery |
| BL-104 | client | Guests cannot reach search at all — every search route sits behind a customer-only route guard and `/search` is not in the public-path list, even though the underlying endpoints are already anonymous. | search-and-discovery gap-11 | open | search-and-discovery, public-front-door |

## Minor (115)

| ID | Area | Item | Origin | Status | Blocks |
|----|------|------|--------|--------|--------|
| BL-105 | client | Admin hub renders a raw i18n key as the role label — the translation keys were never added. | account-and-settings gap-5 | open | account-and-settings |
| BL-106 | client | Customer hub navigates with a hand-built locale prefix instead of the shared navigation helper — works today, breaks silently if `localePrefix` ever changes. | account-and-settings gap-11 | open | account-and-settings |
| BL-107 | client | UNVERIFIED: a system-theme user may see one light/dark flip after hydration (SSR cookie vs. post-hydration MUI resolution can disagree). | account-and-settings gap-12 | open | account-and-settings |
| BL-108 | product | No notification-preference surface exists anywhere — the settings row only deep-links to the notification centre. | account-and-settings gap-9 | open | account-and-settings, notifications |
| BL-109 | client | The pin is a required address-form field, so the API's "no pin" state and its UI badge are unreachable except via direct API calls or seed data. | addresses-and-map gap-6 | open | addresses-and-map |
| BL-110 | ops | Geography is seeded one city per province; districts exist only for Tehran, so the region cascade is a two-step formality everywhere else. | addresses-and-map gap-7 | open | addresses-and-map |
| BL-111 | client | `GET geo/tree` is live but unused — the region cascade makes three round trips instead of one. | addresses-and-map gap-8 | open | addresses-and-map |
| BL-112 | client | Map-pin RTL geometry is dodged via inline style, untested for RTL drift beyond the keyless fallback path. | addresses-and-map gap-9 | open | addresses-and-map |
| BL-113 | docs | No `product/` doc describes address entry, the map-pin picker, or the Neshan integration. | addresses-and-map gap-10 | open | addresses-and-map |
| BL-114 | client | `/fa/admin/notifications` is a placeholder stub and a true orphan — no link reaches it and no bell is mounted in the admin shell. | admin-backoffice gap-9, notifications gap-8 | open | admin-backoffice, notifications |
| BL-115 | client | `POST holidays/delete_holiday` is unwired — the admin console offers no delete affordance for a holiday row. | admin-backoffice gap-10 | open | admin-backoffice |
| BL-116 | ops | `admin_search/rebuild_index` and `admin_booking_requests/expire` are reachable only by curl, and both 403 for a seeded admin anyway. | admin-backoffice gap-12 | open | admin-backoffice, booking-request |
| BL-117 | client | `useAdminCapabilities` hides tabs but never blocks the route itself — every admin console stays URL-reachable regardless of capability; server enforcement is currently just the blanket BL-001 403. | admin-backoffice gap-13, H-14 | open | admin-backoffice |
| BL-118 | client | A 429 on OTP verify renders as "wrong code" — the empty error body leaves nothing to branch on, unlike the phone-step's correct 429 handling. | auth-login-otp gap-6 | open | auth-login-otp |
| BL-119 | contract | The client ignores the server's OTP metadata (`codeLength`, `expiresInSeconds`) and hardcodes its own — the 60-second code expiry is never shown, only the 120-second resend cooldown. | auth-login-otp gap-7 | open | auth-login-otp |
| BL-120 | docs | Stale client comments claim the server exposes no OTP code length or machine-readable failure code — it exposes both. | auth-login-otp gap-8 | open | auth-login-otp |
| BL-121 | client | A signed-in user visiting `/login` sees the phone form again instead of being redirected away, unlike `/welcome`. | auth-login-otp gap-10 | open | auth-login-otp |
| BL-122 | product | The OTP SMS template is not WebOTP-conformant, so the client's WebOTP autofill ships but never fires; landing this is a Kavenegar-dashboard template edit (ops action), not a code change. | auth-login-otp gap-14, REQ-039 | open | auth-login-otp |
| BL-123 | ops | No seeded account is role-less, so `/fa/select-role` has no natural path in the demo world. | auth-login-otp gap-15, onboarding-nurse gap-10 | open | onboarding-customer, onboarding-nurse |
| BL-124 | client | Client hardcodes English toasts for 401/403/5xx/network errors — no i18n dictionary. | H-13 | open | (all authenticated flows) |
| BL-125 | client | Admin read-only consoles are inconsistently client-guarded — some pages check capabilities only to gate mutation buttons, `admin/audit` has zero check, and no shared `CapabilityGuard` component exists. | H-14 | open | admin-backoffice |
| BL-126 | docs | `bnpl/apis/clientApi.ts` and its header comment both claim the by-request BNPL read 404s — it exists and returns 200, a stale comment blocking a partial de-mock. | bnpl gap-5 | open | bnpl-installments |
| BL-127 | client | `acceptBnplSchedule` derives request status client-side from the order status, mislabelling a failed/reverted/cancelled order as awaiting-payment — the expired-window branch can never fire on the real path. | bnpl gap-6 | open | bnpl-installments |
| BL-128 | product | `providerCommissionReversedAmount` is left null on a reverted BNPL order — nothing in the UI or admin surfaces the resulting commission shortfall. | bnpl gap-8 | open | bnpl-installments |
| BL-129 | ops | Seeded BNPL eligibility status (`"approved"`) is not in the closed status vocabulary on either side of the wire. | bnpl gap-9 | open | bnpl-installments |
| BL-130 | server | D3 KYC inputs are half-wired — the client sends `nationalId`/`mobile`/`consent`, but eligibility only reads the mobile; legal consent is collected and silently discarded. | bnpl gap-10, REQ-023 (residue note) | open | bnpl-installments |
| BL-131 | ops | Nothing fires the BNPL provider webhook in dev, so even a successfully initiated real order would never reach `settled` — settlement is webhook-driven by design. | bnpl gap-11 | open | bnpl-installments |
| BL-132 | client | `/fa/bookings/checkout/bnpl/gateway` is an orphan route on the real path, reachable only from the mock and gated outside development. | bnpl gap-12 | open | bnpl-installments |
| BL-133 | client | No client path produces a `disputed` booking status — it renders in the status map but nothing ever raises one. | booking-lifecycle-evv gap-4 | open | booking-lifecycle-evv |
| BL-134 | contract | The nurse's today-feed session row carries no service/variant label — every row says patient name + visit index only. | booking-lifecycle-evv gap-8, REQ-052 | open | booking-lifecycle-evv |
| BL-135 | contract | A booking list row has no `patientId` — a booking card can never deep-link to the care record. | booking-lifecycle-evv gap-9, care-circle-patients gap-13, REQ-057 | open | booking-lifecycle-evv, care-circle-patients |
| BL-136 | client | `POST booking_sessions/cancel/{id}` is unwired — a nurse cannot cancel a single visit from the UI. | booking-lifecycle-evv gap-11 | open | booking-lifecycle-evv |
| BL-137 | docs | `services/bookings/apis/serverApi.ts` is dead code with no importer, and its own doc-block still claims the domain is mock-primary. | booking-lifecycle-evv gap-13 | open | booking-lifecycle-evv |
| BL-138 | contract | No structured rejection-reason code exists — the client runs a seven-keyword heuristic on free text to decide whether to offer "retry with the same nurse," and it silently misses any other wording. | booking-request gap-5, REQ-044 | open | booking-request |
| BL-139 | contract | `variantLabel` is typed as client-augmented/optional though the server always serves it, and the list row still lacks `variantPrice`/`variantPriceUnit`, so the nurse inbox card can't show the money without opening the detail. | booking-request gap-6/7, REQ-050 (residue) | open | booking-request |
| BL-140 | contract | `patientAge` is served on every nurse-inbox row but never modelled client-side — a pure client-side type-widening fix. | booking-request gap-8 | open | booking-request |
| BL-141 | client | The customer's pending-request list doesn't poll while the detail view does, so countdowns on `/bookings` go stale until a manual refresh. | booking-request gap-9 | open | booking-request |
| BL-142 | client | `useBookingRequest` has no auth gate (unlike both list hooks) — a hard reload before hydration can flash an error card. | booking-request gap-10 | open | booking-request |
| BL-143 | product | The nurse sees the patient's display name pre-accept, wider than the integration doc's "notes + coarse address only" — confirm this is the intended stage-1 boundary. | booking-request gap-12 | open | booking-request |
| BL-144 | client | The client's 22 in-memory mocks are module-scoped and reset on reload/HMR — a mocked cancel demo doesn't survive navigating to the refund-status screen. | cancellation-and-refunds gap-17 | open | cancellation-and-refunds |
| BL-145 | contract | `preview.refundableSessionIds` doesn't exist on the wire (only `sessions[]`) — harmless today, but the client type lies. | cancellation-and-refunds gap-7 | open | cancellation-and-refunds |
| BL-146 | docs | The refunds `clientApi.ts` doc-block is stale — claims REQ-019/020/021 are contract gaps; all three routes exist and return 200. | cancellation-and-refunds gap-8 | open | cancellation-and-refunds |
| BL-147 | server | No `GET refunds/my` — the wallet «استردادها» tab renders empty on the real path. | cancellation-and-refunds gap-11, REQ-048 | open | cancellation-and-refunds |
| BL-148 | client | `/fa/admin/finance` is a hub with a single payouts tile — no refunds, clawbacks, or invoice-issue surface. | cancellation-and-refunds gap-13 | open | cancellation-and-refunds |
| BL-149 | client | Client discards a field the server does serve on the visit-note read — the "N of M tasks" summary chip can never render on the real path. | care-circle-patients gap-6 | open | care-circle-patients |
| BL-150 | contract | `deniedReason` enum mismatch on patient-record access denial — server sends `not_authorized`, client/docs declare `no_access`/`not_found`. | care-circle-patients gap-7, patient-care-records gap-5 | open | care-circle-patients, patient-care-records |
| BL-151 | docs | Stale client comments assert patient-record endpoints "have no backend" — all exist, are authorized, and were probed 200. | care-circle-patients gap-8/9, patient-care-records gap-12 | open | care-circle-patients, patient-care-records |
| BL-152 | ops | Seeded care plans are empty and seeded patients have no relation/conditions — three of four record tabs would be blank the moment the mock flag flips. | care-circle-patients gap-11/12, patient-care-records gap-9 | open | care-circle-patients, patient-care-records |
| BL-153 | docs | No `product/business/` file covers patient care records at all — no documented rule for append-only, encryption, or the clinical-access gate. | care-circle-patients gap-14, patient-care-records gap-10 | open | care-circle-patients, patient-care-records |
| BL-154 | client | Archiving a patient is optimistic with no undo — a stale/cross-tenant id 404s and the card silently reappears with a generic toast. | care-circle-patients gap-15 | open | care-circle-patients |
| BL-155 | client | The only way to demo patient-record access-denial uses a hardcoded sentinel patient id — untested against how the real server denies. | patient-care-records gap-11 | open | patient-care-records |
| BL-156 | contract | `sessionCount` is served nullable but typed non-null client-side; UNVERIFIED how the i18n label renders a null count. | checkout-and-payment gap-4 | open | checkout-and-payment |
| BL-157 | docs | Payment client constants/comments are stale — claim a live endpoint 404s, claim the mock is still primary, and hardcode a platform-fee rate that no longer matches the server's seeded value. | checkout-and-payment gap-6 | open | checkout-and-payment |
| BL-158 | server | Invoice carries no payment method, transaction reference, or seller fiscal identity — those rows never render on the real path. | checkout-and-payment gap-9, REQ-049 | open | checkout-and-payment |
| BL-159 | server | `GET invoices/{bookingId}` tenancy is enforced only inside the query handler; UNVERIFIED whether a foreign customer actually gets 404. | checkout-and-payment gap-11 | open | checkout-and-payment |
| BL-160 | server | Staff authorization is inconsistent by layer for tickets — the handler-level role set includes Support/Finance, but the controller attribute blocks them; two different definitions of "staff" in one request path. | messaging-tickets gap-3 | open | messaging-tickets |
| BL-161 | client | `POST`/`DELETE tickets/{id}/participants` are unwired — no UI can attach a third party to a thread. | messaging-tickets gap-7 | open | messaging-tickets |
| BL-162 | client | `close`/`reopen` are live and working server-side but unreachable client-side — one shared flag gates them alongside the genuinely-missing `assign`, so two already-shippable features stay dark. | messaging-tickets gap-8, REQ-063 (residue) | open | messaging-tickets |
| BL-163 | server | `POST tickets/{id}/assign` is a phantom endpoint the client already targets. | messaging-tickets gap-9, REQ-063 | deferred (trigger: ticket assignment prioritized) | messaging-tickets |
| BL-164 | docs | Stale client comment claims the server has no field for `clientMessageId` echo — it is sent and echoed today. | messaging-tickets gap-10 | open | messaging-tickets |
| BL-165 | contract | Client `TicketAuthorRole` declares `system`; the server's role vocabulary doesn't — one side must move. | messaging-tickets gap-11 | open | messaging-tickets |
| BL-166 | product | Auto-created coordination tickets open with no first message — both actors see an empty thread with no explanation of what it's for. | messaging-tickets gap-12 | open | messaging-tickets |
| BL-167 | contract | Ticket message history is unpaginated by contract — a long thread has no incremental read. | messaging-tickets gap-13 | open | messaging-tickets |
| BL-168 | client | Ticket attachments are fully designed but gated off behind a flag. | messaging-tickets gap-14, REQ-060 | deferred (trigger: attachment upload prioritized) | messaging-tickets |
| BL-169 | contract | `refund_issued`/`refund_processed` and `review_moderated`/`review_published` are near-miss names on either side of the same contract. | notifications gap-4 | open | notifications |
| BL-170 | server | `verification_expiry_prompt` is dispatched with a null data payload — even a matching client branch couldn't route it. | notifications gap-5 | open | notifications, nurse-verification |
| BL-171 | client | `NotificationBellPopover` (full mark-read + deep-link logic) is exported but mounted nowhere — dead UI. | notifications gap-9 | open | notifications |
| BL-172 | client | Notification "load more" isn't real pagination — it grows the page size and refetches the whole feed every time. | notifications gap-10 | open | notifications |
| BL-173 | server | Notification retention (90d/24h) is a hardcoded constant rather than a `platform_configs` row, against the repo's config-is-rows convention. | notifications gap-11 | open | notifications |
| BL-174 | client | Both notification pages are client components with no `generateMetadata`, departing from the thin-RSC-page convention. | notifications gap-12 | open | notifications |
| BL-175 | product | No unread-notification cap or archive — the feed grows until the 90-day sweep; unread rows are never separately swept. | notifications gap-13 | open | notifications |
| BL-176 | docs | `IVariantSnapshotSerializer`'s doc-comment names the wrong table for the frozen JSON snapshot. | nurse-catalog-and-pricing gap-6 | open | nurse-catalog-and-pricing |
| BL-177 | server | Category `iconKey` and description fields are null for all seeded categories — the catalog grid falls back to a generic icon with no explainer copy. | nurse-catalog-and-pricing gap-7 | open | nurse-catalog-and-pricing |
| BL-178 | server | `sessionCount` is unvalidated against `priceUnit` — nothing stops an invalid combination from being saved. | nurse-catalog-and-pricing gap-9 | open | nurse-catalog-and-pricing |
| BL-179 | client | The mock payout store's fixture booking ids deep-link into the (now real) bookings screens and 404. | nurse-earnings-and-payouts gap-2 | open | nurse-earnings-and-payouts |
| BL-180 | contract | The client sends `Idempotency-Key` on payout generate/retry, which the controller never reads — decorative and misleading to a reader. | nurse-earnings-and-payouts gap-13 | open | nurse-earnings-and-payouts |
| BL-181 | server | `holidayShifted` is always false — `PayoutBatchDto` carries no such flag, so a bank-holiday date shift can never be shown. | nurse-earnings-and-payouts gap-15 | open | nurse-earnings-and-payouts |
| BL-182 | product | No payout forecast — the nurse dashboard's "next batch" line renders nothing on the real path. | nurse-earnings-and-payouts gap-16, REQ-053 | open | nurse-earnings-and-payouts |
| BL-183 | client | Only one bank-rail failure reason is mapped to a label; every other reason falls back to a generic message. | nurse-earnings-and-payouts gap-17 | open | nurse-earnings-and-payouts |
| BL-184 | docs | Stale payouts doc-blocks assert three live endpoints don't exist server-side — all three return 200. | nurse-earnings-and-payouts gap-18 | open | nurse-earnings-and-payouts |
| BL-185 | client | Duplicate React keys on the nurse-service-areas search results whenever the BL-077 duplicate-listing bug fires. | nurse-service-areas gap-2, search-and-discovery gap-2 | open | nurse-service-areas, search-and-discovery |
| BL-186 | docs | The service-areas integration doc states a conflict rule that the live server doesn't enforce (probed 200 where it claims a conflict). | nurse-service-areas gap-4 | open | nurse-service-areas |
| BL-187 | client | No warning that removing a nurse's last service area de-lists her from search — the confirm-dialog copy doesn't distinguish this case. | nurse-service-areas gap-6 | open | nurse-service-areas |
| BL-188 | ops | Remove-service-area was left end-to-end unverified this pass (token expired mid-probe); code-traced only. | nurse-service-areas gap-7 | open | nurse-service-areas |
| BL-189 | ops | The shared demo DB has drifted from the seeder's own definitions (nurse 1 now has 5 service areas, not the seeded 3) and the idempotent seeder will never correct it. | nurse-service-areas gap-8 | open | nurse-service-areas |
| BL-190 | client | Verification step model omits `isRequired` though the wire serves it, so the "X of Y" progress meter would be wrong the moment an optional step type is added. | nurse-verification gap-9 | open | nurse-verification |
| BL-191 | contract | No `submittedAt` on the nurse verification status, and no per-step detail on the public trust-badge read. | nurse-verification gap-10, REQ-043, REQ-055 | open | nurse-verification, search-and-discovery |
| BL-192 | ops | Seeded verification data leaves two of six catalog step types unexercisable in the demo world. | nurse-verification gap-12 | open | nurse-verification |
| BL-193 | client | `POST customer_profiles/avatar` is live and implemented but no client code calls it — a customer can never set a photo. | onboarding-customer gap-2 | open | onboarding-customer, account-and-settings |
| BL-194 | client | `preferredLanguage` write silently commits a default `'fa'` the customer never chose, the first time they open and save the language sheet. | onboarding-customer gap-4/6 | open | onboarding-customer |
| BL-195 | client | `avatarUrl` on the nurse-profile upsert input is dead on the real path — populated by the form but never sent (harmless today, since the multipart route already persisted it separately). | onboarding-nurse gap-5 | open | onboarding-nurse |
| BL-196 | docs | The onboarding integration doc states duplicate-IBAN returns 409; the handler actually returns 400 with a field error. | onboarding-nurse gap-6 | open | onboarding-nurse |
| BL-197 | client | The bank-account page polls a stuck ownership inquiry forever, with no ceiling and no timeout copy. | onboarding-nurse gap-11 | open | onboarding-nurse |
| BL-198 | server | The HTTP merchant-of-record resolver (`internal/bookings/{id}/center`) is `DynamicPermission`-gated and 403s for both seeded admins — reachable only in-process today. | partner-center gap-7 | open | partner-center |
| BL-199 | server | Two merchant-of-record resolvers can disagree — BNPL reads a platform config value while invoicing resolves the actual center, so a MoR center's BNPL order and invoice could name different sellers. | partner-center gap-10 (folded severity, see BL-013) | open | partner-center, bnpl-installments |
| BL-200 | ops | Seeded partner-center invoices carry null Moadian reference/PDF fields — the settlement view's document columns have nothing real to render. | partner-center gap-11 | open | partner-center |
| BL-201 | product | Center self-onboarding (write-then-masked IBAN) is deferred and has never been exercised on a real route. | partner-center gap-12 | deferred (trigger: center self-service onboarding prioritized) | partner-center |
| BL-202 | client | Welcome page's Open-Graph image is Latin-only — no Persian glyph rendering for the `fa` share card. | public-front-door gap-7 | open | public-front-door |
| BL-203 | client | `/fa/welcome` has no in-app link at all — reachable only as the `/` rewrite body or by typing the URL. | public-front-door gap-8 | open | public-front-door |
| BL-204 | ops | `NEXT_PUBLIC_SITE_URL` is unset in dev, so `robots.txt`/`sitemap.xml`/OG tags fall back to localhost (dev-only caveat, `.env.production` is correct). | public-front-door gap-9 | open | public-front-door |
| BL-205 | ops | `next dev` misrepresents this flow entirely — stale prerenders serve ahead of the middleware; only a production build reproduces real behavior. | public-front-door gap-1 | open | public-front-door |
| BL-206 | server | No anonymous rate limit on the public search/profile reads — falls back to the shared global per-IP limiter. | public-front-door gap-4, search-and-discovery gap-12, REQ-066 (residue) | open | public-front-door, search-and-discovery |
| BL-207 | client | `GET nurses/{id}/review_tags` (the "% of reviewers said X" rollup) and `POST reviews/{id}/tags` are both unwired — no chip-aggregate view and no post-submit tag amendment. | reviews gap-5/6 | open | reviews |
| BL-208 | server | The low-rating support alert is raised on submit but has no reachable triage surface — reads sit behind BL-001. | reviews gap-10 | open | reviews |
| BL-209 | docs | Stale review doc comments claim the mock is still primary; the real client is live and the 14.6KB mock module is dead behind the flag. | reviews gap-11 | open | reviews |
| BL-210 | server | `distanceKm` is always null on search results — the covering index carries no coordinate, so the distance chip can never render. | search-and-discovery gap-7 | open | search-and-discovery |
| BL-211 | contract | `topReviewTag` and `variantDisplayName` are never served on search results — dead code on the client's tag chip and no multi-variant collapse. | search-and-discovery gap-8, REQ-040 | open | search-and-discovery |
| BL-212 | client | `nurseGender` is hardcoded `'female'` in the public-profile mapping because the DTO carries no gender field — unused today, but a live lie in the typed model. | search-and-discovery gap-9, REQ-042 | open | search-and-discovery |
| BL-213 | server | `attributeChips` is empty for every seeded nurse, and a masked reviewer name maps to an empty string — the profile snippet renders a bare separator with nothing around it. | search-and-discovery gap-14 | open | search-and-discovery |
| BL-214 | contract | No nurse-facing read-back of submitted credential details (INO-on-file, specialties) — the returning-nurse form can't hydrate from the server. | REQ-056 | open | nurse-verification |
| BL-215 | contract | Policy numbers (dispute-window hours, cancellation lead-time, refund ETA) have no public/authenticated read — the client keeps them single-sourced in a local constants file. | REQ-065, ui-phase-12 follow-up 3 | open | cancellation-and-refunds |
| BL-216 | contract | No typed address/variant snapshot objects — `BookingDetailDto` still carries opaque JSON strings, forcing the client's defensive multi-key parse in three separate places. | REQ-045, ui-phase-6 follow-up 2, ui-phase-7 follow-up 2 | open | booking-lifecycle-evv, checkout-and-payment |
| BL-217 | client | Two admin-only multi-field dialogs (`GrantRoleDialog` in `admin/roles`, `PreviewBatchDialog` in `admin/payouts`) still hold raw `useState` instead of react-hook-form — the only genuine residue of the app-wide form migration. | iteration-2 #7 | open | admin-backoffice |
| BL-218 | client | `H-01` residue: the auth-gate root-cause fix landed, but `client/middleware.ts` was never migrated to Next 16's `src/proxy.ts` as the original fix prescribed, and `outputFileTracingRoot` was never added alongside `turbopack.root` — a production build may still warn from the stray root-level lockfile. | H-01 | open | auth-login-otp |
| BL-219 | docs | Rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel now that `user-secrets` is confirmedly removed — filed by phase 2 with a worklist (`appsettings.*.json`, `StartupSecretsGuard.PlaceholderMarkers`, `StartupSecretsGuardTests.cs`, `docs/rules/server/structure.md`); the two `.githooks/*` files in the original worklist no longer exist (phase 7 dropped the pre-commit hook — see [decisions.md](decisions.md)). Optional cleanup, not urgent — the name is a load-bearing sentinel and the mechanism is already documented correctly elsewhere. | C-2 (open-contradictions.md) | deferred (trigger: none — cleanup-of-convenience) | — |

## Deferred (43)

These are recorded decisions with pull-triggers, not bugs — carried here (per this phase's Status vocabulary)
for phase 5's `roadmap/deferred.md` to pick up. Grouped by theme; each origin id is greppable in the source
handoff/report files for the full context.

| ID | Area | Item | Origin | Trigger | Blocks |
|----|------|------|--------|---------|--------|
| BL-220 | ops | Redis (shared cache, cross-instance scheduler/money lock) is not deployed — every current seam is correct only for a single instance. | handoff-after-refinement-phase-7/8, refinement-phase-7/8/9 follow-ups | running more than one instance | — |
| BL-221 | server | Elasticsearch `INurseSearch` backend + outbox feeder not built — SQL search is the real MVP implementation. | handoff-after-backend-phase-7, handoff-after-refinement-phase-7/8/9, backend-phase-7 follow-up | SQL search shows real strain | search-and-discovery |
| BL-222 | product | `IAnalyticsSink` / an analytics warehouse is not built — events aren't routed anywhere durable yet. | handoff-after-backend-phase-1, handoff-after-refinement-phase-9 | product pulls it | — |
| BL-223 | server | A real holiday-calendar feed (vs. the seeded/admin-CRUD calendar) is not built. | handoff-after-backend-phase-1, handoff-after-refinement-phase-9 | product pulls it | — |
| BL-224 | server | `organizations`/`organization_nurses` (employer model) tables are not built. | handoff-after-refinement-phase-9, frontend-phase-15 follow-up 2 | product pulls it | — |
| BL-225 | product | `fraud_flags` / ML fraud scoring is not built — manual suspension + support alerts cover this for now. | handoff-after-backend-phase-6/14, handoff-after-refinement-phase-9 | product pulls it | — |
| BL-226 | server | `recurring_booking_schedules` is not built. | handoff-after-refinement-phase-9, frontend-phase-15 follow-up 2 | product pulls it | — |
| BL-227 | server | `bnpl_settlement_entries` (tranched settlement) is modelled but not built. | handoff-after-backend-phase-12, handoff-after-refinement-phase-9 | product pulls it | bnpl-installments |
| BL-228 | product | Nurse availability slots/exceptions (soft scheduling guidance) are not built. | handoff-after-backend-phase-5/7, handoff-after-refinement-phase-9 | product pulls it | booking-request |
| BL-229 | product | Customer national-ID KYC collection is deliberately not built — the product decision is to never gate browsing/booking on it. | handoff-after-backend-phase-3, handoff-after-refinement-phase-9 | never, by design (informational) | — |
| BL-230 | server | A geography bulk-import feed (`IGeoDataImporter`) is deferred — the idempotent seed + admin CRUD is sufficient for MVP. | handoff-after-backend-phase-4, backend-phase-4 follow-up | product pulls it | addresses-and-map |
| BL-231 | product | Holiday/surge pricing, a distinct Companionship pricing tier, and tiered per-category commission are all deferred. | handoff-after-backend-phase-5, backend-phase-5 follow-up | product pulls it | nurse-catalog-and-pricing |
| BL-232 | product | GPS-radius "nurses near me" map discovery is explicitly not planned — coverage stays named-district-only. | handoff-after-backend-phase-4/7 | none — permanent product decision | search-and-discovery |
| BL-233 | server | Automated MoH/INO license lookup and a professional-liability-insurance verification step type are both deferred. | handoff-after-backend-phase-6 | a lookup portal exists / product pulls it | nurse-verification |
| BL-234 | ops | The credential-expiry scan and the EVV no-show sweep both run only via manual admin endpoints — no cron calls either yet. | handoff-after-backend-phase-6/9, backend-phase-6 follow-up | scheduled-ops phase | nurse-verification, booking-lifecycle-evv |
| BL-235 | server | SMS.ir/Ghasedak SMS adapters are not built — only Kavenegar's real-SMS path is wired (attempting either throws at startup by design, never silently mocks). | handoff-after-refinement-phase-8, refinement-phase-8 follow-up | a second SMS vendor is needed | auth-login-otp |
| BL-236 | ops | Finnotech/Moadian token-exchange refresh and the Moadian signing certificate are not wired — both are deploy-time actions once real credentials exist. | handoff-after-refinement-phase-8 | going to a real Moadian integration | partner-center |
| BL-237 | ops | The Moadian reconciliation poll and the refund-settlement (BNPL processing→succeeded) poll are both thin/manual — no scheduled cron for either. | handoff-after-backend-phase-11, handoff-after-refinement-phase-7/8, refinement-phase-6/7/8 follow-ups | scheduled-ops phase | cancellation-and-refunds, bnpl-installments |
| BL-238 | server | Per-provider-code BNPL revert is incomplete — the refund path only drives the SnappPay default regardless of the transaction's actual provider. | handoff-after-refinement-phase-8, refinement-phase-8 follow-up | a second BNPL provider goes live | bnpl-installments |
| BL-239 | product | A dedicated merchant-of-record center-settlement payout path (تسهیم split leg) is deferred per decision 6.6. | handoff-after-refinement-phase-8, refinement-phase-6/8 follow-ups | product pulls it | partner-center |
| BL-240 | ops | The weekly payout-batch cron generates automatically, but processing a batch stays a deliberate, explicit admin action by design (not a bug). | handoff-after-refinement-phase-7, backend-phase-13 follow-up | none — permanent product decision | nurse-earnings-and-payouts |
| BL-241 | product | On-demand/instant nurse payout withdrawal and per-nurse payout-frequency configuration are both deferred (MVP is one fixed weekly cadence for everyone). | handoff-after-backend-phase-13, frontend-phase-12 follow-up 4 | 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-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 | **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 |
| BL-249 | server | Legacy `UserRefreshTokens` (gRPC auth path) still exists alongside the real session-based REST auth. | backend-phase-2 follow-up | gRPC moves to sessions, or is dropped | auth-login-otp |
| BL-250 | client | The ESLint unused-vars gate is a repo-wide no-op — 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 golden rule 11 claiming it does. | frontend-phase-13 follow-up 1 | dedicated infra task | — |
| BL-251 | client | PWA/offline caching (Workbox) is unbuilt — marked "maybe" in the original product backlog, i.e. optional. | product/notes open-questions.md | product pulls it | — |
| BL-252 | server | Optional short-TTL cache over hot search result pages was shipped no-cache at MVP. | backend-phase-7 follow-up | search read latency becomes a real problem | search-and-discovery |
| BL-253 | server | A single explicit DB transaction for the payment-webhook confirm path (currently two commits, kept safe via idempotency + a forward-only guard) is flagged as future hardening. | backend-phase-10 follow-up | `IUnitOfWork` grows a transaction scope | checkout-and-payment |
| BL-254 | server | The partner-center dashboard's sponsored-nurse list is capped at 50 with an exact count — needs pagination if a center's roster grows past that. | backend-phase-15 follow-up | a center exceeds 50 sponsored nurses | partner-center |
| BL-255 | server | `Bookings`/`Invoices.partner_center_id` columns exist with no DB-level FK — only `nurse_profiles.partner_center_id` got one, per the shipping phase's own Definition of Done. | backend-phase-15 follow-up, backend-phase-11 follow-up | a data-integrity pass on partner-center columns | partner-center |
| BL-256 | client | Skeleton→content crossfade exists as a one-line-per-screen pattern but was never retrofitted onto every list/detail page. | ui-phase-12 follow-up 1 | a dedicated visual-polish pass | — |
| BL-257 | client | No true desktop search layout — a full responsive pass beyond the phone-width frame was explicitly deferred post-chain. | ui-phase-12 follow-up 2 | product decides to support desktop | search-and-discovery |
| BL-258 | client | Persian OG image variant is Latin-only by deliberate scope cut on `/welcome` and other share cards. | ui-phase-13 follow-up, public-front-door gap-7 | Persian social sharing becomes a priority | public-front-door |
| BL-259 | server | List-row EVV presence indicator (a lightweight "currently checked in" flag on the booking list DTO) was never built — needs a product/API decision to avoid an N+1 read. | ui-phase-5 follow-up 1 | product prioritizes a list-level EVV signal | booking-lifecycle-evv |
| BL-260 | client | Web-push for new nurse booking requests remains deferred — the 15-second poll is the only freshness mechanism. | ui-phase-7 follow-up 5, REQ-054 | push infra (service worker + VAPID + dispatch rail) is built | booking-request |
| BL-261 | client | Ticket attachments' composer button is fully built but gated off pending the upload/signed-URL backend. | REQ-060 (see also BL-168) | attachment backend delivered | messaging-tickets |
| BL-262 | product | Real payment-gateway/Shaparak logos near the pay CTA are deferred — no licensed assets yet; a generic lock-icon trust notice stands in. | ui-phase-6 follow-up 5 | licensed gateway assets obtained | checkout-and-payment |
