# 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**).
