cleanup phases 6
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# 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**).
|
||||
Reference in New Issue
Block a user