# After backend-phase-6 — nurse verification & credentials are live (vendors mocked) The trust engine now exists. A nurse opens a **verification checklist**, clears automated checks (identity-KYC, Shahkar, bank-ownership) and uploads documents for manual credential steps; an admin reviews, decides, and — when every required step passes — the platform flips `nurse_profiles.is_verified` **inside a single transaction**, making the nurse bookable. Credential **numbers are encrypted and never leave the API**; the public **trust badge** exposes only the credential **types** a nurse holds. Contract: [`dev/contracts/domains/verification.md`](../../../contracts/domains/verification.md); machine schema: `dev/contracts/openapi/swagger.v1.json` (refreshed for b6 — all 15 endpoints). **All vendor/money calls are mocked** behind deterministic DI seams. ## What the frontend (f5-b6) can now build - **The nurse verification checklist screen** — `GET api/v1/nurse_verification` returns `status`, `isBookable`, `blockingSteps[]`, and `steps[]` (each with `code`, `displayName`, `status`, `isAutomated`, `expiresAt?`, `failureReason?`). `POST api/v1/nurse_verification/submit` opens/seeds it (idempotent). Requires a nurse profile first (b3 `nurse_profiles/upsert`). Render the checklist off `steps`, gate the "you're bookable" state on `isBookable`, and surface `blockingSteps` as the to-do list. - **Identity submit (automated)** — `POST api/v1/nurse_verification/steps/identity_kyc/run` `{ nationalId, livenessPayload? }` → `RunStepResult` (`stepStatus`, `failureReason?`). A pass populates the nurse's national id; drive the next steps off it. - **Shahkar & bank runs (automated)** — `POST .../steps/shahkar_match/run` (no body; **needs KYC passed**) and `POST .../steps/bank_account_verification/run` (no body; **needs KYC + a primary bank account from b3**). Both return `RunStepResult`. Shared-SIM comes back as a clean `failed` + reason (not an error) — show it. - **Document upload for manual steps** — two-step: `POST .../steps/{stepId}/upload_url` `{ contentType, fileName? }` → `{ objectStorageKey, uploadUrl }`; PUT the file to `uploadUrl`; then `POST .../steps/{stepId}/documents` `{ objectStorageKey, integrityHash, contentType, fileSizeBytes, originalFileName? }` → the step moves to `in_review`. Manual steps only (an automated step 400s the upload). - **The credentials / under-review / rejected states** — render each `step.status` (`not_started`/`pending`/`in_review`/`passed`/`failed`/`expired`) with the right affordance; `in_review` = "with our team", `failed`/`expired` = show `failureReason` + a redo path, `passed` = done. - **The public trust badge (f6)** — `GET api/v1/nurses/{nurseId}/trust_badge` (**anonymous**) → `isVerified`, `approvedAt?`, `credentialTypes[]`. Types only — never a credential number. Back the nurse-profile trust chip / verified marker with it. ## Live endpoints (all under `api/v1`, action-style, camelCase bodies) Nurse (`[Authorize]`, nurse-scoped): `nurse_verification/submit`, `GET nurse_verification`, `nurse_verification/steps/{stepId}/upload_url`, `nurse_verification/steps/{stepId}/documents`, `nurse_verification/steps/identity_kyc/run`, `nurse_verification/steps/shahkar_match/run`, `nurse_verification/steps/bank_account_verification/run`. Admin step-types (dynamic-permission): `GET admin_verification_step_types`, `POST admin_verification_step_types`, `DELETE admin_verification_step_types/{id}`. Admin review (dynamic-permission): `GET admin_verifications`, `GET admin_verifications/{nurseVerificationId}`, `admin_verifications/steps/{stepId}/decide`, `admin_verifications/{nurseVerificationId}/suspend`, `admin_verifications/scan_expiring`. Public (`[AllowAnonymous]`): `GET nurses/{nurseId}/trust_badge`. ## Rules baked into the API (don't fight them client-side) - **`isBookable` / `isVerified` are read-only, server-derived.** Never infer verification client-side — read `VerificationStatusDto.isBookable` (nurse view) or `TrustBadgeDto.isVerified` (public view). The aggregate `nurse_verifications.status` is the single source of truth; `is_verified` is flipped only inside the finalize transaction (and reversed on suspend/expiry). - **Credential numbers never cross the wire.** `NurseCredentialDto` / `TrustBadgeDto` carry **types** and metadata only. Don't build a UI that expects to display a number. - **Automated vs manual steps drive different UI.** `step.isAutomated=true` → a `/run` button; `false` → the upload flow (`upload_url` → PUT → `documents`). The list tells you which. - **Ordering / prerequisites** — Shahkar needs identity-KYC passed; bank verification needs KYC **and** a primary bank account (b3). Calling out of order → **400**; disable/guide accordingly. - **Shared-SIM and vendor fails are `200` with `stepStatus:"failed"` + `failureReason`**, not HTTP errors — surface the reason, don't treat as a crash. (Shared-SIM also raises an internal support alert.) - **Documents come back as short-lived signed URLs** (`VerificationDocumentDto.url`) — fetch/display fresh; don't cache the URL. - **Refresh after `select_role`** still applies (nurse scoping reads the role claim in the token). - **Routes are action-style POST** (`nurse_verification/submit`, `admin_verifications/steps/{id}/decide`, …), ids from the route, camelCase bodies — see the contract for the full list. ## Schema / migration New **`verif`** schema, one additive migration, **5 tables**: `nurse_verifications` (header; `status` = single source of truth), `verification_step_types` (seeded catalog — six stable codes), `verification_steps` (one per required step-type; snapshots `is_automated`), `verification_documents` (**metadata only** — bytes never in the DB), `nurse_credentials` (`credential_number` **encrypted**, never serialized). The only derived boolean is `nurse_profiles.is_verified`, flipped/reversed transactionally. ## What's mocked All vendor/money calls (deterministic seams; test values in the contract): - `IShahkarVerifier` → `MockShahkarVerifier` (pass unless shared-SIM `09120000000` / mismatch id `1111111111`). - `IIdentityKycProvider` → `MockIdentityKycProvider` (passes any well-formed 10-digit id except `0000000000`). - `ICredentialVerifier` → `MockCredentialVerifier` (manual-admin default; `verification_method=manual`). - **Reused:** `IBankAccountOwnershipVerifier` (b3; mismatch IBAN `IR000000000000000000000000`), `IObjectStorage` (b0; local-disk signed URLs), `IFieldEncryptor` (b0; encrypts `credential_number`). See [`reports/mocks-registry.md`](../../reports/mocks-registry.md) for the make-it-real steps. ## Deferred to later phases (do not build against these yet) - **Scheduled expiry cron** (`CredentialExpiryScannerJob`) → the scan logic ships as `ScanExpiringCredentialsCommand`; today only the admin `scan_expiring` endpoint triggers it. - **Automated MoH/INO license lookup** (`ICredentialVerifier`, `verification_method=api`) → deferred. - **`fraud_flags` / ML fraud scoring** → deferred. - **Professional-liability-insurance step** → addable later as a step-type row (no schema change). - **b7 (search & matching)** reads `nurse_profiles.is_verified` for `nurse_search_index.is_searchable` — b6 owns the flip, b7 owns the index (**not built here**).