# Flows — what is implemented, and how to test it One file per **user-meaningful journey**. Each answers three questions and no others: what it does, what is real versus mocked, and the exact steps to walk it against a running stack. **Start here → [testing-setup.md](testing-setup.md)** for bring-up, the demo accounts and the OTP. Then pick a row below. > Last verified: 2026-08-02 against commit `c841bde`, against a **booted stack** — API on > `http://localhost:5002` against the shared remote SQL Server, client on `http://localhost:3000` (and a > production build on `:3001`), all 8 demo accounts logged in over the real phone-OTP round-trip. --- ## Status table `Client` / `Server` are independent on purpose. **A real UI on a mocked service is the trap this atlas exists to expose** — six flows below are exactly that. | Flow | Actor | Status | Client | Server | Gaps | File | | --- | --- | --- | --- | --- | --- | --- | | auth-login-otp | all | **built** | real | real | 15 | [→](auth-login-otp.md) | | public-front-door | guest | partial | real | real | 10 | [→](public-front-door.md) | | onboarding-customer | customer | partial | real | real | 9 | [→](onboarding-customer.md) | | care-circle-patients | customer | partial | partial | real | 15 | [→](care-circle-patients.md) | | addresses-and-map | customer | partial | real | real | 10 | [→](addresses-and-map.md) | | onboarding-nurse | nurse | partial | partial | real | 11 | [→](onboarding-nurse.md) | | nurse-service-areas | nurse | partial | real | real | 8 | [→](nurse-service-areas.md) | | nurse-verification | nurse + admin | **mocked** | mock | partial | 12 | [→](nurse-verification.md) | | nurse-catalog-and-pricing | nurse | partial | real | real | 9 | [→](nurse-catalog-and-pricing.md) | | search-and-discovery | customer | partial | partial | real | 14 | [→](search-and-discovery.md) | | booking-request | customer + nurse | partial | real | real | 12 | [→](booking-request.md) | | checkout-and-payment | customer | partial | partial | partial | 11 | [→](checkout-and-payment.md) | | bnpl-installments | customer | **mocked** | mock | partial | 12 | [→](bnpl-installments.md) | | booking-lifecycle-evv | nurse + customer | partial | real | real | 13 | [→](booking-lifecycle-evv.md) | | cancellation-and-refunds | customer + admin | **mocked** | mock | partial | 17 | [→](cancellation-and-refunds.md) | | reviews | customer + admin | partial | real | real | 11 | [→](reviews.md) | | patient-care-records | nurse + customer | **mocked** | mock | real | 12 | [→](patient-care-records.md) | | nurse-earnings-and-payouts | nurse + admin | **mocked** | mock | partial | 18 | [→](nurse-earnings-and-payouts.md) | | messaging-tickets | all | partial | partial | real | 14 | [→](messaging-tickets.md) | | notifications | all | partial | real | real | 13 | [→](notifications.md) | | admin-backoffice | admin | **mocked** | partial | partial | 13 | [→](admin-backoffice.md) | | partner-center | partner | **mocked** | mock | partial | 12 | [→](partner-center.md) | | account-and-settings | all | partial | partial | real | 12 | [→](account-and-settings.md) | **1 built · 15 partial · 7 mocked · 0 not started · 0 UNVERIFIED.** 283 gaps recorded — Phase 4's input. `built` = end-to-end real and observed working · `partial` = real end to end with named gaps · `mocked` = the UI is real, the data is fake · `not started` = no implementation. Every row is backed by a code trace, and every money flow plus auth was additionally walked against the running API. --- ## The six things that surprise everyone 1. **No seeded admin can reach any admin endpoint.** All 16 admin GET operations return `403` for both `09120000020` (super_admin) and `09120000021` (finance). `DynamicPermissionService.CanAccess` grants on the **literal** role `"admin"` or a per-controller `DynamicPermission` claim; the demo admins hold `super_admin`/`finance`, and **no code anywhere writes that claim**. The client hides it completely because `USE_ADMIN_MOCK = true`. → [admin-backoffice.md](admin-backoffice.md) 2. **A `false` mock flag does not mean the domain is honest.** `payment` is flag-real with only 2 of 6 operations working; `search` hardcodes `isVerified: true` and `nurseGender: 'female'`; `tickets.getUnreadTotal` is literally `async () => null`. 3. **A `true` mock flag does not mean the real half is missing.** `verification` has 10 of 14 operations live and probed, `admin` 9 of 14, `payouts` 5 of 10 — all suppressed by one shared flag. The nurse's verification status answers correctly to `curl` right now; nothing in the browser calls it. 4. **The seeded demo world is dated 2026-07-26 and has aged out.** No `pending` or `accepted` booking request survived the 60-second expiry job, so the nurse inbox and checkout have nothing seeded to act on. → [testing-setup.md](testing-setup.md#the-seeded-world-and-how-stale-it-is) 5. **No card payment can complete from a browser.** `MockPaymentProvider` redirects to `https://mock-psp.local/pay/…`, a host that does not exist. The server money path is correct and idempotent; only the last hop is unreachable. → [checkout-and-payment.md](checkout-and-payment.md) 6. **The real BNPL rail is dead on the live stack.** `SeedPaymentGatewaysAsync` seeds only a `Standard` gateway, never a `Bnpl` one, so `POST /checkout_bnpl/initiate` returns `400 "No active BNPL gateway is configured."` for every request — verified live. The UI never notices because it runs on a mock. → [bnpl-installments.md](bnpl-installments.md) --- ## Mock-vs-real map — client Derived from code across all 22 `client/src/services/` domains, **153 seam operations**. `Real-half verdict` is about the `clientApi.ts` implementation, independent of the flag: `COMPLETE` = every op maps a published route with no fabrication · `PARTIAL` = ≥1 op fabricates or derives a value · `GAPPED` = ≥1 op targets a route that does not exist server-side. | Domain | Flag | Ops | Real-half verdict | live / proposed / fabricated | | --- | --- | --- | --- | --- | | `addresses` | real | 5 | COMPLETE | 5 / 0 / 0 | | `admin` | **mock** | 14 | GAPPED | 9 / 5 / 0 | | `auth` | real | 6 | COMPLETE | 6 / 0 / 0 | | `bnpl` | **mock** | 7 | GAPPED | 2 / 4 / 1 | | `bookingRequests` | real | 6 | COMPLETE | 6 / 0 / 0 | | `bookings` | real | 7 | COMPLETE | 7 / 0 / 0 | | `catalog` | real | 7 | COMPLETE | 7 / 0 / 0 | | `geography` | real | 3 | COMPLETE | 3 / 0 / 0 | | `notifications` | real | 4 | COMPLETE | 4 / 0 / 0 | | `nurse` (bank) | real | 4 | COMPLETE | 4 / 0 / 0 | | `partnerCenter` | **mock** | 13 | GAPPED | 6 / 7 / 0 | | `patientRecords` | **mock** | 5 | GAPPED | 2 / 3 / 0 | | `patients` | real | 5 | COMPLETE | 5 / 0 / 0 | | `payment` | real | 6 | **GAPPED + PARTIAL — worst real domain** | 2 / 2 / 2 | | `payouts` | **mock** | 10 | GAPPED + PARTIAL | 5 / 4 / 1 | | `profiles` | real | 6 | COMPLETE | 6 / 0 / 0 | | `refunds` | **mock** | 9 | GAPPED | 2 / 7 / 0 | | `reviews` | real | 6 | COMPLETE | 6 / 0 / 0 | | `search` | real | 2 | PARTIAL | 2 / 0 / 3 fields | | `serviceAreas` | real | 3 | COMPLETE | 3 / 0 / 0 | | `tickets` | real | 11 | PARTIAL + GAPPED (gated) | 7 / 3 / 1 | | `verification` | **mock** | 14 | GAPPED + PARTIAL | 10 / 3 / 1 | Totals: **15 real / 7 mock** · ~107 ops map published routes · **34 would 404 if the flag were flipped today** · 12 fabricate or client-derive a value. Three further facts a reader needs: - **20 of 22 mocks hold mutable module state** — a mocked demo that crosses a full page navigation restarts from its seed. - **Two cross-domain mock edges break live paths**: the `refunds` and `bnpl` mocks read the `bookings` mock store, which is seeded only with ids `5001-5005`, and `findBooking` throws `404` for anything else. Real booking ids therefore `404` inside those wizards. - **One production seam breach**: [`nurse/verification/page.tsx:12`](../../client/src/app/[locale]/(private-routes)/nurse/verification/page.tsx) imports `__mockApproveAll`/`__mockRejectStep` from `apis/mockApi` unconditionally. The render is gated; the module edge is not, so the mock ships in every build. ## Mock-vs-real map — server seams **Exactly one `Seams:*:Provider` is set anywhere in the repo — `Seams:Sms:Provider = telegram`.** Every other rail runs on its mock, in development *and* in the deployed stack: `docker-compose.yml` overrides only the relay's `BaseUrl` and the object-storage `RootPath`, never a selector. | Rail | Mock | Real adapter available | Selected today | | --- | --- | --- | --- | | SMS / OTP | `LoggingSmsSender` | `KavenegarSmsSender` · `TelegramSmsSender` | **telegram** | | Card PSP | `MockPaymentProvider` | `ZarinPalPaymentProvider` | mock | | Settlement split (تسهیم) | `MockSettlementSplitProvider` | `ProviderSettlementSplitProvider` | mock | | BNPL | `MockBnplProvider` | `SnappPayBnplProvider` · `DigipayBnplProvider` | mock | | Object storage | `LocalDiskObjectStorage` | `S3ObjectStorage` | mock | | Geocoder | `MockGeocoder` | `NeshanGeocoder` | mock | | Bank transfer (payouts) | `MockBankTransferProvider` | none | mock — **moves no money** | | e-invoicing (مودیان) | `MockMoadianClient` | `MoadianClient` | mock | | Shahkar · e-KYC · IBAN ownership | mocks | Finnotech adapters | mock | | Credential (MoH/INO) · eNamad · review moderation | mocks | **none — deliberate** | mock, always | | Search | *(no mock)* | `SqlNurseSearch` | real | Two rails **throw at startup** instead of falling back to the mock: `Seams:Sms:Provider = smsir|ghasedak`, and `Search:Backend` set to anything but `sql`. And the PSP selector is not a token match — `ServiceCollectionExtension.cs:174` treats *any* non-`mock` string as ZarinPal, so a typo silently selects a real gateway. ## Corrections to `mocks-registry.md` [`archive/build-chain/working-context/reports/mocks-registry.md`](../../archive/build-chain/working-context/reports/mocks-registry.md) was cross-checked row by row against the code: **the whole frontend section — 26 rows plus its prose header — and 17 disagreements were found.** The registry now carries a banner pointing here. The systemic cause: its **"Config flag … default `true`"** column was never updated after the refinement-phase-4 de-mock, although the same file's prose header and Status column were. | Kind | Count | Detail | | --- | --- | --- | | Stale flag default | 8 | `patients`, `profiles`, `nurse` (bank), `geography`, `addresses`, `serviceAreas`, `catalog`, `reviews`, `notifications` rows all still say "default `true`"; the code says `false` | | Actively wrong about behaviour | 3 | `profilesClientApi.uploadAvatar` "throws 501" — it does a real multipart upload · `AddressMapPicker` "not a real map, no Neshan tiles, no network" — it renders real Leaflet Neshan tiles when the key is set · the `payment` row records the flip as clean and never records the 4 broken ops it produced | | Wrong in both directions at once | 1 | One row's flag column says all-mock and its own Status column says all-real; reality is 2 real / 3 mock | | Missing entirely | 3 | `SearchApi`, `BookingRequestsApi` (the exact store behind the BNPL breakage), and `geography/neshan.ts`'s direct third-party `fetch` calls | | Internal contradiction | 1 | The registry says the REQ-027 endpoints exist; `patientRecords/apis/clientApi.ts` says none exist. Needs a server-side ruling | | Verified and **holding** | 15+ | Including all 7 remaining flag values, the EVV GPS default, and the deleted card mock-gateway harness | Backend seam rows were **not** re-audited row-by-row; the current server picture is the seam table above. --- ## Coverage accounting **All 14 business areas are covered.** Each maps to a primary flow; none is orphaned. | Area | Primary flow | Area | Primary flow | | --- | --- | --- | --- | | 01 Actors & Onboarding | `auth-login-otp`, `onboarding-*` | 08 Payments & Escrow | `checkout-and-payment` | | 02 Nurse Verification | `nurse-verification` | 09 Installments / BNPL | `bnpl-installments` | | 03 Catalog & Pricing | `nurse-catalog-and-pricing` | 10 Payouts | `nurse-earnings-and-payouts` | | 04 Search & Matching | `search-and-discovery` | 11 Reviews, Trust & Safety | `reviews` | | 05 Booking & Scheduling | `booking-request`, `booking-lifecycle-evv` | 12 Messaging & Emergencies | `messaging-tickets` | | 06 EVV / Service Delivery | `booking-lifecycle-evv` | 13 Tax, Invoicing & Legal | `partner-center` ⚠ **weakest** | | 07 Cancellation & Refunds | `cancellation-and-refunds` | 14 Notifications & Admin | `notifications`, `admin-backoffice` | **Area 13 is the thin one.** Invoicing and VAT are split across `checkout-and-payment` (the per-booking invoice) and `admin-backoffice` (the مودیان reconciliation); no flow owns the legal/tax story end to end. Phase 4 backlog item. **All 22 client service domains are covered** by at least one flow (see the map above). **All 83 client routes** are reachable from a flow file's Screens table; the 9 routes with no service domain are static pages, and the orphans (nothing links to them) are recorded in the flow that owns their area. **One flow has no product source:** `account-and-settings` is a UI-phase-9 decision with no `product/business/` file behind it. `public-front-door`'s depth decision lives in [`product/notes/open-questions.md`](../../product/notes/open-questions.md) rather than a business area. --- ## Conventions these files follow 1. Every file carries `> Last verified: against `. A file without one is a claim, not a fact. 2. Every status is backed by a code trace (route → hook → seam → endpoint → controller → handler) with `file:line` evidence. The money flows and auth were additionally walked against the running API. 3. What could not be checked is written with an explicit `UNVERIFIED:` prefix and a reason. 4. **API shapes are not restated here** — [docs/integration/](../integration/index.md) owns them, and every flow links to its domain file. **Business rules are not restated** — [product/](../../product/index.md) owns them; a flow states the number and links to its source. 5. Each file stays under 200 lines. `testing-setup.md` is a deliberate exception: it is the single page you hand someone, and splitting it would defeat that.