Files
baya-monorepo/dev/shared-working-context/reports/backend-phase-6-report.md
T
hamid 1c266523bc backend phase 6: nurse verification & credentials (mocked vendors)
The trust engine. New `verif` schema (5 tables) + a data-driven verification
pipeline: steps are rows (6 seeded step-types), not a code enum.

- nurse_verifications.status is the single source of verification truth;
  nurse_profiles.is_verified is flipped ONLY inside the finalize transaction
  (VerificationAggregator: tracked verification + tracked profile -> one commit)
  and reversed on suspension/expiry — no in-between state.
- is_automated snapshotted onto each step at submit; steps seeded from active
  required step-types; automated runs (identity-KYC, Shahkar, IBAN ownership)
  find their step by code.
- users.national_id populated only on identity-KYC pass; Shahkar + IBAN owner
  compare against it (money-mule guard); shared-SIM -> shared_sim support alert.
- Documents are metadata-only behind signed URLs; credential_number encrypted
  and never serialized; public trust badge exposes credential TYPES, not numbers;
  holder-name cross-checked against the verified identity before recording.
- Admin-triggered credential-expiry scan reverts lapsed steps, re-gates
  bookability, raises a verification_expired alert + verification_expiry_prompt
  notification (scheduled cron deferred; config key
  verification_expiry_scan_cadence_hours).

Three new mock vendor seams (IShahkarVerifier / IIdentityKycProvider /
ICredentialVerifier) behind DI; reuses b3 IBankAccountOwnershipVerifier and
b0 IObjectStorage/IFieldEncryptor. 15 endpoints across 4 controllers.

Two migrations (tables + step-type seed). 154 tests pass, zero new warnings.
Contract dev/contracts/domains/verification.md + swagger snapshot refreshed;
handoff/report/mocks-registry updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:39:32 +03:30

10 KiB

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 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 failureshared_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/submit200; GET /api/v1/nurse_verificationstatus: "pending", isBookable: false, one seeded step per active required step-type, each with its isAutomated.
  2. Identity KYC passesPOST /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 0000000000stepStatus: "failed" + failureReason (still 200).
  3. Shahkar matchPOST /api/v1/nurse_verification/steps/shahkar_match/run (no body) → passed; shahkar_verified_at set. A nurse on the shared-SIM phone 09120000000handled 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 credentialPOST /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 rejectionReason400.
  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 badgeGET /api/v1/nurses/{nurseId}/trust_badgeisVerified: true, approvedAt, credentialTypes: ["moh_competency_license", …] (types only, never numbers). Unknown nurse → 404.
  10. Suspend & expiryPOST /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:

  • IShahkarVerifierMockShahkarVerifier — 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.
  • IIdentityKycProviderMockIdentityKycProvider — national-id + liveness: passes any well-formed 10-digit national id except the configured fail id 0000000000. Registry row: IIdentityKycProvider.
  • ICredentialVerifierMockCredentialVerifier — 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 (all 15 routes, DTO shapes, the four enums, failure cases, side effects, mock test values, worked example). swagger.v1.json refresheddev/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 scoringdeferred.
  • 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.