# 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: <date> against <commit>`. 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.
