# Flow — nurse-verification > Last verified: 2026-08-02 against commit `c841bde` **Actor(s):** nurse · admin · (public, read-only trust badge) · **Status:** mocked **Client:** mock · **Server:** partial (nurse half real and live; admin half real code, unreachable — 403) **Business source:** [product/business/02-nurse-verification.md](../../product/business/02-nurse-verification.md) **Integration:** [docs/integration/domains/verification.md](../integration/domains/verification.md) ## What it does A nurse proves who she is and what she is licensed to do — identity, phone binding, professional credentials, bank ownership — and an admin reviews the manual parts. Until the pipeline says `approved` the nurse is not bookable and does not appear in search. This is the flow the whole marketplace's trust claim rests on. **This is the atlas's clearest "real UI on a mocked service in front of a live server" trap.** Every nurse screen renders in-browser fixtures from [`verification/apis/mockApi.ts`](../../client/src/services/verification/apis/mockApi.ts) because `USE_VERIFICATION_MOCK = true` ([`constants.ts:9`](../../client/src/services/verification/constants.ts)) — while the server's nurse half answers correctly, right now, to a `curl`. Nothing in the browser ever calls it. The one flag also holds the working nurse half hostage to the broken admin half. ## Screens | Step | Route | Component / notes | | --- | --- | --- | | B3 status hub | `/fa/nurse/verification` | [`page.tsx`](../../client/src/app/[locale]/(private-routes)/nurse/verification/page.tsx) — the canonical view of the **one cached status query** (`useVerificationStatus`). `not_started` CTA → grouped checklist → terminal approved panel. Renders mock-only approve/reject buttons (`:84-95`) | | checklist | (same) | `VerificationChecklist.tsx` + [`verificationSteps.ts`](../../client/src/app/[locale]/(private-routes)/nurse/verification/verificationSteps.ts) — data-driven; groups server steps into هویت / مدارک حرفه‌ای / بانک. Prepends a **synthetic `mobile_verified` step** (`MOBILE_STEP`, id `0`, always `passed`) that is not a server step — it is satisfied at phone-OTP login | | B4 identity | `/fa/nurse/verification/identity` | national id (10-digit + checksum) + ID-card image + liveness selfie → `identity_kyc` run, chained `shahkar_match`. The two images are **local captures**, never uploaded | | B5 credentials | `/fa/nurse/verification/credentials` | one `DocumentUpload` per manual step **in the status** (`MANUAL_CREDENTIAL_CODES`, `page.tsx:32`) + INO number, specialties, credential dates | | B6 under review | `/fa/nurse/verification/review` | a second focused view of the **same cached query** — never a second fetch | | admin queue | `/fa/admin/verification` | `useVerificationQueue`; per-step wire rows folded to one row per nurse | | admin case | `/fa/admin/verification/[nurseId]` | per-step pass/reject + `DocumentViewer`; prev/next off the queue's cached page order | | admin group root | `/fa/admin/trust` | nav only, no data | | public | `/fa/search/nurse/:nurseId` | `TrustBadge` + `VerificationPanel` read `getTrustBadge` — see [search-and-discovery.md](search-and-discovery.md) | `DocumentUpload` resolves its resting state as `state === 'success' || (state === 'idle' && existingDoc != null)` ([`DocumentUpload.tsx:132`](../../client/src/components/DocumentUpload/DocumentUpload.tsx)) — a just-finished upload wins over stale server metadata, so a re-upload never snaps back to the old file name. ## API Shapes, enums and the two vocabularies live in [docs/integration/domains/verification.md](../integration/domains/verification.md). All 20 paths below were confirmed present in the live OpenAPI doc (178 paths). | Call | Endpoint | Notes | | --- | --- | --- | | status (B3/B5/B6) | `GET /api/v1/nurse_verification` | **live, probed 200.** `clientApi.ts:157` → `NurseVerificationController.cs:32` | | start | `POST …/submit` | wired | | presign / confirm | `POST …/steps/{stepId}/upload_url` · `POST …/steps/{stepId}/documents` | wired; XHR PUT direct to storage + SHA-256 integrity hash (`clientApi.ts:180-208`) | | automated runs | `POST …/steps/{identity_kyc\|shahkar_match\|bank_account_verification}/run` | wired; each is a separately switchable seam | | credential details | `POST …/credential_details` | **endpoint EXISTS** (`NurseVerificationController.cs:49-52`) but `clientApi.ts:212` is an empty no-op — see gaps | | public badge | `GET /api/v1/nurses/{nurseId}/trust_badge` | **live, anonymous, probed 200** | | admin queue | `GET /api/v1/admin_verifications` | **403 for every seeded admin** | | admin case | `GET /api/v1/admin_verifications/{nurseVerificationId}` | 403 | | admin decide | `POST /api/v1/admin_verifications/steps/{stepId}/decide` | 403. **Per-step only** — approval emerges when the last required step passes | | suspend · scan_expiring | `POST …/{id}/suspend` · `POST …/scan_expiring` | exist server-side, **no client caller** | | step-type catalog | `GET/POST /api/v1/admin_verification_step_types`, `DELETE …/{id}` | exist, **no client caller**; probed **403** | | approve / reject / document URL | `POST …/{id}/approve`, `…/reject`, `GET …/documents/{id}/url` | **proposed, do not exist** (REQ-034) — the admin case page wires CTAs to all three | ## Rules that must hold | Rule | Source | | --- | --- | | `status` is the source of truth; `nurse_profiles.is_verified` is **derived** and flips only inside the transaction that confirms every required step passed (INV-18) | [business/02](../../product/business/02-nurse-verification.md); `AdminReviewStepCommand.Handler.cs:74-78` | | The step catalog is **data, not code** — `verification_step_types` rows. 6 seeded, all `is_required`. Adding a regulatory step is one INSERT | [business/02](../../product/business/02-nurse-verification.md) §(a); `VerificationStepTypeSeed.cs` | | The server computes `isBookable` and names `blockingSteps`; the client must **never** derive bookability from the step array | [integration/verification.md](../integration/domains/verification.md) | | Search visibility is one gate: `is_verified AND is_accepting_bookings AND status != suspended AND variant.is_active` (INV-17) | `SearchIndexMaintainer.cs:177,248` | | `expired` is a normal step state, not an error — the credential-expiry scan runs every **24 h** (`verification_expiry_scan_cadence_hours`) and re-gates bookability | [business/02](../../product/business/02-nurse-verification.md) | | The trust badge exposes credential **types** only; `credential_number` is encrypted and never serialized | [business/02](../../product/business/02-nurse-verification.md) §(c-bis) | | INO membership locks once submitted (it feeds the public badge) | [integration/verification.md](../integration/domains/verification.md) | ## How to test 1. Log in as **09120000003** (مریم احمدی — the deliberately unverified nurse) — see [testing-setup.md](testing-setup.md). Boot the API with `Seams__Sms__Provider=mock` or `request_otp` 500s. 2. Open `/fa/nurse/verification`. **Expect:** the B3 grouped checklist renders and a «شبیه‌سازی بررسی ادمین» block with approve/reject buttons appears at the bottom. That block is the mock tell — it only renders when `USE_VERIFICATION_MOCK` is true. 3. Walk B4 → B5, then press the mock «تأیید» button. **Expect:** the hub flips to the green approved panel with a «انتشار خدمات» CTA. **This changed nothing on the server** — reload the page and the mock store resets to its seed. 4. Now read the server truth for the same nurse: `curl --noproxy '*' http://localhost:5002/api/v1/nurse_verification -H "Authorization: Bearer $T_09120000003"`. **Expect (probed 2026-08-02, HTTP 200):** `status: "in_review"`, `isBookable: false`, `blockingSteps: ["moh_competency_license","criminal_record"]`, and 4 steps — `identity_kyc` passed, `shahkar_match` passed, `moh_competency_license` in_review, `criminal_record` pending. Compare it with what step 3 showed you; they are unrelated. 5. Public badge, no auth: `curl --noproxy '*' http://localhost:5002/api/v1/nurses/1/trust_badge`. **Expect:** `{"nurseId":1,"isVerified":true,"approvedAt":"2026-07-26T10:52:18…","credentialTypes":["criminal_record","moh_competency_license"]}`. Nurse 3 returns `isVerified:false`, `credentialTypes:[]`. 6. Admin half: open `/fa/admin/verification` as **09120000020**. **Expect:** a populated queue — all of it in-browser fixtures. The live endpoint is **403**: `curl --noproxy '*' "http://localhost:5002/api/v1/admin_verifications?page=1&page_size=5" -H "Authorization: Bearer $T_09120000020"` → `403` (probed). Same for `/api/v1/admin_verification_step_types`. **Seeded-world limits.** No admin can approve anything on the real path (the RBAC gap). Nurse 1 is already `approved` but returns `steps: []`, so there is no approved-with-checklist case to look at. Nurse 3 carries only 4 of the 6 seeded step types — `ino_membership` and `bank_account_verification` have no step rows at all, so the bank branch of the journey cannot be walked against real data. Workaround for the whole flow: none today; the honest demo is the mock, and the honest server check is `curl`. ## Known gaps - `USE_VERIFICATION_MOCK = true` ([`verification/constants.ts:9`](../../client/src/services/verification/constants.ts)) suppresses a working real nurse half — 7 nurse-side ops map live, probed-200 routes. One flag covers nurse + public badge + 6 admin ops, so the nurse half cannot be flipped independently. - `submitCredentialDetails` is an empty no-op (`verification/apis/clientApi.ts:212`) even though `POST /api/v1/nurse_verification/credential_details` exists and `SubmitCredentialDetailsCommand` matches `CredentialDetailsInput` field-for-field. **Flipping the flag today would silently drop every nurse's INO number and specialties.** The client comment, `useSubmitCredentials.ts` and [integration/verification.md](../integration/domains/verification.md) all still say "no nurse-facing endpoint" — stale. - Every `admin_verifications` and `admin_verification_step_types` route returns **403** for `super_admin`/`finance` (`DynamicPermissionService.CanAccess`). The admin review half of this flow is untestable end to end; the mock hides it completely. - [`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 but the module edge is not — the mock is bundled into the nurse verification route in every build. This is the one production seam breach in the client. - With the flag flipped there is **no** way for a nurse to observe the approved flip in a demo: the mock admin controls disappear and the real admin queue 403s. - `foldQueueRows` (`clientApi.ts:74-96`) folds a per-step wire page to per-nurse items and leaves `stepsPassed: 0`, `stepsTotal: 0`, `hasExpiringCredential: false`, `counts` undefined; `total` stays the per-step count, so the queue's pager is nominal. A nurse's steps can straddle a page boundary (REQ-034, REQ-062). - `approveVerification`, `rejectVerification` and `getDocumentSignedUrl` target routes that do not exist (REQ-034). The admin case page wires visible approve/reject CTAs to two of them — they would 404 on the real path. - `AdminVerificationsController` `suspend` and `scan_expiring`, and all three `admin_verification_step_types` routes, have **no client caller** — no suspension UI and no step-catalog editor, so the "data-driven catalog" rule has no admin surface. - The wire serves `isRequired` per step (confirmed live) but `VerificationStep` (`verification/types.ts:62-71`) omits it, and `progressCounts` assumes every step is required. Adding one optional step type would make the "X از Y" meter wrong. - No `submittedAt` on `VerificationStatusDto` (REQ-055) → B6 omits the submitted-at line. `TrustBadgeDto` carries no per-step detail (REQ-043) → the public `VerificationPanel` is a summary only. Both confirmed against the live responses. - The admin case route folder is `[nurseId]` but the value it passes is a **`nurseVerificationId`** (`[nurseId]/page.tsx:74`). Typing `/fa/admin/verification/3` opens verification-case 3, not nurse 3. - Seeded data: nurse 1 (`approved`) returns `steps: []`, and nurse 3 has no `ino_membership` or `bank_account_verification` step rows — two of the six catalog steps are unexercisable in the demo world.