12 KiB
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 Integration: docs/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 because
USE_VERIFICATION_MOCK = true (constants.ts:9) —
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 — 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 — 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 |
DocumentUpload resolves its resting state as state === 'success' || (state === 'idle' && existingDoc != null)
(DocumentUpload.tsx:132) — 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. 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; 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 §(a); VerificationStepTypeSeed.cs |
The server computes isBookable and names blockingSteps; the client must never derive bookability from the step array |
integration/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 |
The trust badge exposes credential types only; credential_number is encrypted and never serialized |
business/02 §(c-bis) |
| INO membership locks once submitted (it feeds the public badge) | integration/verification.md |
How to test
- Log in as 09120000003 (مریم احمدی — the deliberately unverified nurse) — see
testing-setup.md. Boot the API with
Seams__Sms__Provider=mockorrequest_otp500s. - 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 whenUSE_VERIFICATION_MOCKis true. - 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.
- 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_kycpassed,shahkar_matchpassed,moh_competency_licensein_review,criminal_recordpending. Compare it with what step 3 showed you; they are unrelated. - 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 returnsisVerified:false,credentialTypes:[]. - Admin half: open
/fa/admin/verificationas 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) 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.submitCredentialDetailsis an empty no-op (verification/apis/clientApi.ts:212) even thoughPOST /api/v1/nurse_verification/credential_detailsexists andSubmitCredentialDetailsCommandmatchesCredentialDetailsInputfield-for-field. Flipping the flag today would silently drop every nurse's INO number and specialties. The client comment,useSubmitCredentials.tsand integration/verification.md all still say "no nurse-facing endpoint" — stale.- Every
admin_verificationsandadmin_verification_step_typesroute returns 403 forsuper_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:12imports__mockApproveAll/__mockRejectStepfromapis/mockApiunconditionally. 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 leavesstepsPassed: 0,stepsTotal: 0,hasExpiringCredential: false,countsundefined;totalstays 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,rejectVerificationandgetDocumentSignedUrltarget 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.AdminVerificationsControllersuspendandscan_expiring, and all threeadmin_verification_step_typesroutes, 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
isRequiredper step (confirmed live) butVerificationStep(verification/types.ts:62-71) omits it, andprogressCountsassumes every step is required. Adding one optional step type would make the "X از Y" meter wrong. - No
submittedAtonVerificationStatusDto(REQ-055) → B6 omits the submitted-at line.TrustBadgeDtocarries no per-step detail (REQ-043) → the publicVerificationPanelis a summary only. Both confirmed against the live responses. - The admin case route folder is
[nurseId]but the value it passes is anurseVerificationId([nurseId]/page.tsx:74). Typing/fa/admin/verification/3opens verification-case 3, not nurse 3. - Seeded data: nurse 1 (
approved) returnssteps: [], and nurse 3 has noino_membershiporbank_account_verificationstep rows — two of the six catalog steps are unexercisable in the demo world.