cleanup phases 6
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# Backend Phase 6 — Nurse verification & credentials (mocked vendors) — report
|
||||
|
||||
**Status:** complete · build clean (0 new code warnings) · `dotnet test Baya.sln` green (**153 pass**).
|
||||
**Vendors are mocked** (three new DI seams + reused b3/b0 seams) — see [What is mocked](#what-is-mocked).
|
||||
|
||||
## What was built
|
||||
The trust engine the whole marketplace gates on — a data-driven verification pipeline, the admin review
|
||||
queue, the structured credential registry, the transactional `nurse_profiles.is_verified` flip, an
|
||||
admin-triggered credential-expiry scanner, and the public trust badge. One additive migration (new **`verif`**
|
||||
schema, **5 tables**), **15 endpoints across 4 controllers**, and **3 new mock vendor seams**.
|
||||
|
||||
- **Schema** (new **`verif`** schema): `nurse_verifications` (the header; `status` = the **single source of
|
||||
verification truth**), `verification_step_types` (the seeded catalog of possible steps),
|
||||
`verification_steps` (one per required step-type per nurse; snapshots `is_automated`),
|
||||
`verification_documents` (metadata only — **bytes never in the DB**), `nurse_credentials`
|
||||
(`credential_number` **encrypted**, never serialized). `nurse_profiles.is_verified` is the **only derived
|
||||
boolean**, flipped **only inside the finalize transaction**.
|
||||
- **Six seeded, stable step-type codes:** `identity_kyc`, `shahkar_match`, `moh_competency_license`,
|
||||
`ino_membership`, `criminal_record`, `bank_account_verification` (three of them credential-bearing).
|
||||
- **Nurse pipeline** (`NurseVerificationController`, `[Authorize]` + nurse role in handler, tenancy-scoped):
|
||||
`submit` (upsert header + seed one step per active required step-type; **idempotent**), `GET` status (the
|
||||
checklist + aggregate + `blockingSteps` + `isBookable`), `upload_url` / `documents` (manual steps →
|
||||
`in_review`), and three automated `/run` endpoints — `identity_kyc` (populates `users.national_id` +
|
||||
`national_id_verified_at`), `shahkar_match` (requires KYC; **shared-SIM = explicit handled failure** →
|
||||
`shared_sim` alert; sets `shahkar_verified_at`), `bank_account_verification` (reuses the b3
|
||||
`IBankAccountOwnershipVerifier`; **money-mule guard** — holder national id must equal the verified nurse
|
||||
national id; on match sets `matched_national_id=1`).
|
||||
- **Admin step-type catalog** (`AdminVerificationStepTypesController`, dynamic-permission, `sensitive`
|
||||
rate-limit): cached list (generation token), upsert (snake_case `code`, immutable once in use, dup →
|
||||
**409**), deactivate (`is_active=false`, **never hard delete**).
|
||||
- **Admin review** (`AdminVerificationsController`, dynamic-permission, `sensitive` rate-limit): queue
|
||||
(`in_review` default; docs carry **signed GET URLs**), case detail (steps + docs + credentials + identity
|
||||
name), `decide` (manual steps; on approving a credential-bearing step records an **encrypted**
|
||||
`nurse_credentials` row, **holder-name cross-check** vs identity → mismatch **400** with no credential,
|
||||
`criminal_record` requires `expiresAt`; writes an `audit_logs` record; **re-aggregates**, may flip
|
||||
`is_verified`), `suspend` (`status=suspended` + **reverse `is_verified=0` in one transaction** + audit),
|
||||
and `scan_expiring` (reverts lapsed steps → `expired`, `verification_expired` alert +
|
||||
`verification_expiry_prompt` notification, re-gates bookability).
|
||||
- **Public trust badge** (`NursesController`, `[AllowAnonymous]`): `GET nurses/{id}/trust_badge` returns
|
||||
`isVerified` + `approvedAt` + the **credential TYPES held** (never numbers); cached (short TTL, evicted on
|
||||
suspension/expiry/decision); `404` unknown nurse.
|
||||
- **The scan logic ships** as `ScanExpiringCredentialsCommand`; the **scheduled cron is deferred** — the
|
||||
admin `scan_expiring` endpoint is the entry point (config `verification_expiry_scan_cadence_hours`, int,
|
||||
default `24`).
|
||||
|
||||
## What is now testable — and exactly how (the phase §7 steps)
|
||||
Run the API (`dotnet run --project src/API/Baya.Web.Api/...`) against a reachable SQL Server; use Swagger/curl.
|
||||
The seam mocks are **deterministic** — use the test national ids / IBAN / phone below.
|
||||
1. **Open the checklist** — as a nurse (with a b3 nurse profile), `POST /api/v1/nurse_verification/submit`
|
||||
→ `200`; `GET /api/v1/nurse_verification` → `status: "pending"`, `isBookable: false`, one seeded step per
|
||||
active required step-type, each with its `isAutomated`.
|
||||
2. **Identity KYC passes** — `POST /api/v1/nurse_verification/steps/identity_kyc/run`
|
||||
`{ "nationalId": "1234567891" }` → `stepStatus: "passed"`; the nurse's `users.national_id` +
|
||||
`national_id_verified_at` are populated. The configured fail id `0000000000` → `stepStatus: "failed"` +
|
||||
`failureReason` (still `200`).
|
||||
3. **Shahkar match** — `POST /api/v1/nurse_verification/steps/shahkar_match/run` (no body) → `passed`;
|
||||
`shahkar_verified_at` set. A nurse on the shared-SIM phone `09120000000` → **handled failure** + a
|
||||
`shared_sim` support alert row. Running it **before** KYC → **400**.
|
||||
4. **Bank ownership** — with a **primary** `nurse_bank_accounts` row (b3),
|
||||
`POST /api/v1/nurse_verification/steps/bank_account_verification/run` → on match sets
|
||||
`matched_national_id=1`; the mismatch IBAN `IR000000000000000000000000` → failed. No primary account → **400**.
|
||||
5. **Manual document upload** — for a credential step, `POST steps/{stepId}/upload_url`
|
||||
`{ "contentType": "application/pdf" }` → `{ objectStorageKey, uploadUrl }`; PUT the bytes to `uploadUrl`;
|
||||
`POST steps/{stepId}/documents` `{ objectStorageKey, integrityHash, contentType, fileSizeBytes }` → the
|
||||
step moves to `in_review` and a metadata row is stored (no bytes in the DB). An automated step rejects the
|
||||
upload → **400**.
|
||||
6. **Admin review queue** — as admin, `GET /api/v1/admin_verifications` (default `status=in_review`) →
|
||||
the pending step with **signed GET URLs** on its documents; `GET /api/v1/admin_verifications/{id}` → the
|
||||
full case incl. the identity name for cross-check.
|
||||
7. **Approve a credential** — `POST /api/v1/admin_verifications/steps/{stepId}/decide`
|
||||
`{ approve: true, credentialNumber, holderName: "<verified identity name>", issuingAuthority, issuedAt }`
|
||||
→ records an **encrypted** credential (`credentialId` returned; the number is never serialized back),
|
||||
writes an audit record, re-aggregates. A **holder-name mismatch → 400** with **no** credential recorded;
|
||||
a `criminal_record` approval **without `expiresAt` → 400**; `approve:false` without `rejectionReason` →
|
||||
**400**.
|
||||
8. **is_verified flips** — once every required step is `passed`/`approved`, the re-aggregate flips
|
||||
`nurse_profiles.is_verified=1` **in one transaction**; `GET /api/v1/nurse_verification` now reads
|
||||
`status: "approved"`, `isBookable: true`.
|
||||
9. **Public trust badge** — `GET /api/v1/nurses/{nurseId}/trust_badge` → `isVerified: true`, `approvedAt`,
|
||||
`credentialTypes: ["moh_competency_license", …]` (**types only**, never numbers). Unknown nurse → **404**.
|
||||
10. **Suspend & expiry** — `POST /api/v1/admin_verifications/{id}/suspend` `{ reason }` sets
|
||||
`status=suspended` and **reverses `is_verified=0`** in one transaction (+ audit); the trust badge
|
||||
updates. `POST /api/v1/admin_verifications/scan_expiring` reverts lapsed time-limited steps to `expired`,
|
||||
raises a `verification_expired` alert + `verification_expiry_prompt` notification, and re-gates
|
||||
bookability (`ScanExpiringResult{ scannedSteps, revertedNurses }`).
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
Three **new** seams (all deterministic, all mocked here) + three **reused** seams:
|
||||
- `IShahkarVerifier` → `MockShahkarVerifier` — phone↔national-id match: **pass** unless the shared-SIM phone
|
||||
`09120000000` (→ shared-SIM handled failure) or the mismatch national id `1111111111`. Registry row:
|
||||
`IShahkarVerifier` in [`mocks-registry.md`](mocks-registry.md).
|
||||
- `IIdentityKycProvider` → `MockIdentityKycProvider` — national-id + liveness: passes any well-formed 10-digit
|
||||
national id **except** the configured fail id `0000000000`. Registry row: `IIdentityKycProvider`.
|
||||
- `ICredentialVerifier` → `MockCredentialVerifier` — MoH/INO/criminal-record: the manual-admin default
|
||||
(always `RequiresManualReview` / `verification_method=manual`). Registry row: `ICredentialVerifier`.
|
||||
- **Reused:** `IBankAccountOwnershipVerifier` (b3 — mismatch IBAN `IR000000000000000000000000`),
|
||||
`IObjectStorage` (b0 — local-disk signed PUT/GET URLs), `IFieldEncryptor` (b0 — encrypts
|
||||
`credential_number`).
|
||||
|
||||
## Contracts
|
||||
- **Produced:** [`dev/contracts/domains/verification.md`](../../contracts/domains/verification.md) (all 15
|
||||
routes, DTO shapes, the four enums, failure cases, side effects, mock test values, worked example).
|
||||
`swagger.v1.json` **refreshed** — [`dev/contracts/openapi/swagger.v1.json`](../../contracts/openapi/swagger.v1.json)
|
||||
includes all 15 b6 endpoints.
|
||||
- **Consumed:** `nurse_profiles` + `IBankAccountOwnershipVerifier` (b3), `users.national_id` /
|
||||
`shahkar_verified_at` (b2), `IObjectStorage` / `IFieldEncryptor` / `ICacheService` / CQRS /
|
||||
`OperationResult` / `BaseController` (b0), dynamic-permission + `support_alerts` / `notifications` /
|
||||
`audit_logs` + `sensitive` rate-limit (b1).
|
||||
|
||||
## Docs updated
|
||||
- New contract `dev/contracts/domains/verification.md`; openapi snapshot refreshed.
|
||||
- Handoff `backend/handoff/after-backend-phase-6.md`; status log `backend/STATUS.md` (append).
|
||||
- The three b6 seams (`IShahkarVerifier`, `IIdentityKycProvider`, `ICredentialVerifier`) are the
|
||||
pre-listed rows in `reports/mocks-registry.md` — now realized as seam + fake impl.
|
||||
|
||||
## Follow-ups for later phases
|
||||
- **Scheduled expiry cron** — a `CredentialExpiryScannerJob` hosted scheduler that calls the shipped
|
||||
`ScanExpiringCredentialsCommand` on `verification_expiry_scan_cadence_hours` cadence. **Deferred** — the
|
||||
admin `scan_expiring` endpoint is the manual entry point today.
|
||||
- **Automated MoH/INO license lookup** — behind `ICredentialVerifier` (`verification_method=api`) once a
|
||||
portal exists. **Deferred.**
|
||||
- **`fraud_flags` / ML fraud scoring** — **deferred.**
|
||||
- **Professional-liability-insurance step** — addable later as a `verification_step_types` row (no schema
|
||||
change). **Deferred.**
|
||||
- **b7 (search & matching)** — reads `nurse_profiles.is_verified` for `nurse_search_index.is_searchable`;
|
||||
b6 owns the flip, b7 owns the index.
|
||||
Reference in New Issue
Block a user