7.2 KiB
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; 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_verificationreturnsstatus,isBookable,blockingSteps[], andsteps[](each withcode,displayName,status,isAutomated,expiresAt?,failureReason?).POST api/v1/nurse_verification/submitopens/seeds it (idempotent). Requires a nurse profile first (b3nurse_profiles/upsert). Render the checklist offsteps, gate the "you're bookable" state onisBookable, and surfaceblockingStepsas 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) andPOST .../steps/bank_account_verification/run(no body; needs KYC + a primary bank account from b3). Both returnRunStepResult. Shared-SIM comes back as a cleanfailed+ 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 touploadUrl; thenPOST .../steps/{stepId}/documents{ objectStorageKey, integrityHash, contentType, fileSizeBytes, originalFileName? }→ the step moves toin_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= showfailureReason+ 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/isVerifiedare read-only, server-derived. Never infer verification client-side — readVerificationStatusDto.isBookable(nurse view) orTrustBadgeDto.isVerified(public view). The aggregatenurse_verifications.statusis the single source of truth;is_verifiedis flipped only inside the finalize transaction (and reversed on suspend/expiry).- Credential numbers never cross the wire.
NurseCredentialDto/TrustBadgeDtocarry 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/runbutton;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
200withstepStatus:"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_rolestill 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-SIM09120000000/ mismatch id1111111111).IIdentityKycProvider→MockIdentityKycProvider(passes any well-formed 10-digit id except0000000000).ICredentialVerifier→MockCredentialVerifier(manual-admin default;verification_method=manual).- Reused:
IBankAccountOwnershipVerifier(b3; mismatch IBANIR000000000000000000000000),IObjectStorage(b0; local-disk signed URLs),IFieldEncryptor(b0; encryptscredential_number).
See 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 asScanExpiringCredentialsCommand; today only the adminscan_expiringendpoint 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_verifiedfornurse_search_index.is_searchable— b6 owns the flip, b7 owns the index (not built here).