34 KiB
Backend Phase 6 — Nurse verification & credentials (mocked vendors)
Mission: build the trust engine — the platform's entire brand. A data-driven, platform-owned verification pipeline (steps are rows, not an enum) that mixes automated KYC-vendor checks with manual admin document review, rolls every per-step outcome into one authoritative
nurse_verifications.status, maintains a structured, queryable credential registry (license numbers, authority, holder name, issue/expiry), and transactionally flipsnurse_profiles.is_verifiedthe moment every required step passes (and reverses it on suspension). Documents live in object storage behind signed URLs — only metadata + an integrity hash touch the DB. Every external vendor (Shahkar, identity KYC/liveness, the MoH/INO/criminal-record portals, IBAN ownership) is mocked behind a DI seam so the real swap is implementation-only. After this phase a nurse can finally become bookable.Track: backend · Depends on: b3 (
nurse_profiles,nurse_bank_accounts,IBankAccountOwnershipVerifier), b1 (support_alerts, the typed config accessor), b0 (IObjectStorage,IFieldEncryptor,ICurrentUser, audit interceptor) · Unlocks: search visibility (b7); frontend f5-b6 Before you start, read../_shared/agent-operating-rules.md. It is not optional.
1. Context — where this sits
This is backend phase b6, the verification leg of the build. Balinyaar is a trust-first home-nursing
marketplace; the product docs are blunt that "verified trust is your entire brand." Vetting is
platform-owned, non-optional, and performed at the authoritative source — a nurse's service variants
become bookable only after every required verification step passes. This phase builds that gate: the
6-step pipeline, the admin review queue, the structured credential registry that powers the public trust
badge and renewal alerts, and the one transaction that flips nurse_profiles.is_verified. It sits
between catalog (b5, which built the bookable service variants) and search
(b7, which surfaces only verified nurses) — and it is the hard prerequisite that
makes the nurse_search_index.is_searchable projection meaningful.
What already exists (do not rebuild) — built by prior phases:
nurse_profiles— b3 built the nurse extension offusers(1:1 UNIQUEuser_id), carrying the guardedis_verifiedBIT DEFAULT 0 (no public setter — this phase owns the only write),is_accepting_bookings, the denormalized rating aggregates, and soft-delete. The legacyverification_statuscolumn was deliberately CUT —nurse_verifications.statusis the sole source of verification truth. Do not reintroduce a second copy.nurse_bank_accounts— b3 built the payout-IBAN table (ibanenc,iban_hashUNIQUE,is_primaryfiltered-UNIQUE per nurse,is_verified,matched_national_id,account_holder_from_bank,ownership_vendor_ref,verified_by_admin_id) and theIBankAccountOwnershipVerifierseam (استعلام شبا / Sheba inquiry). This phase'sbank_account_verificationstep couples to that table and reuses that seam — it does not re-register or re-verify bank accounts.users/ identity — b2/b3:users.national_id(enc, NULL until KYC passes),users.gender,shahkar_verified_at/national_id_verified_at(reset to NULL on phone change), and admin RBAC roles for the review queue. The identity-KYC step populatesusers.national_idon pass; Shahkar and IBAN ownership both compare against that verified national-ID.support_alerts— b1 built the alert table + the raise API (built early precisely because verification raises expiry/renewal/shared-SIM alerts). This phase raises alerts; it does not build the table.- The typed, cached config accessor +
platform_configs— b1. Read review-SLA hints, expiry-scan cadence, and any verification thresholds through it; never hardcode. IObjectStorage— b0: presigned/streamed put/get/delete keyed by an opaque storage key, returning a retrievable URL. Verification documents store bytes here. Reuse it.IFieldEncryptor— b0:Encrypt/Decrypt/Hash.credential_numberis encrypted PII through this seam. Reuse it.- The b0 foundation: REST surface,
BaseController,OperationResult<T>, CQRS viamartinothamar/Mediator(not MediatR), Mapster, FluentValidation +ValidateCommandBehavior,ICurrentUser+ the audit-field SaveChanges interceptor, rate limiting,IDateTimeProvider,INotificationDispatcher(in-app write landed via b1).
What this phase introduces: the five verification tables + seed, the nurse/admin capabilities, the
guarded is_verified flip transaction, the credential-expiry scanner, the public trust badge — and three
new seams (IShahkarVerifier, IIdentityKycProvider, ICredentialVerifier). The scheduled cron that
drives the expiry scan is DEFERRED — the scan is admin/manually triggered now (see §3.3).
2. Required reading (do this first)
../_shared/agent-operating-rules.mdand../_shared/backend-conventions-checklist.md— especially the Persistence (encrypted PII through the field encryptor; one config per entity; projected/paginated reads) and Architecture (seams in Application, mocks in Infrastructure) blocks.product/business/02-nurse-verification.md— the business rules: the six steps and what each proves, the data-driven pipeline, the structured credential registry, continuous monitoring (phone-change re-run, expiry re-verify), and the MVP-vs-DEFERRED line (automated MoH/INO lookup, ML fraud scoring, and the liability-insurance step are all DEFERRED).product/data-model/04-verification-and-credentials.md— the canonical schema for the five tables. Mirror these field names exactly (especially thenurse_credentialscolumn list and theUNIQUEconstraints).product/research/verification.md— why the design is what it is: MoH پروانه صلاحیت حرفهای is the single most important credential and already bundles the criminal-record screen; identity is the easy layer (buy a KYC vendor, don't build); license check is manual today because no public B2B API exists; the criminal record is consent-gated to the person (nurse-uploaded + re-requested). This is the source of the "manual today, API later" seam shape.- The identity digest for the cross-domain invariants:
dm_identity_geo_services_verif.md(is_verifiedis write-guarded;national_idNULL until KYC;shahkar_verified_atresets on phone change;nurse_search_index.is_searchabledepends onis_verified). - Code to mirror: b3's
nurse_bank_accountsconfig + theIBankAccountOwnershipVerifierusage; b1'ssupport_alertsraise API + the typed config accessor + the seed-migration pattern (forverification_step_types); b0'sIObjectStorage/IFieldEncryptorusage and aServiceConfiguration/seam registration; any existingFeatures/<Area>/{Commands|Queries}/**for handler structure andOperationResultreturns. - Contract conventions:
../../contracts/conventions/api-conventions.md(envelope, routing) — money is not central here but follow the format. - Prior handoffs:
dev/shared-working-context/backend/handoff/after-backend-phase-3.md,…-1.md,…-0.md, andreports/mocks-registry.md(theIObjectStorage/IBankAccountOwnershipVerifier/IFieldEncryptorrows you reuse, and where to add the three new ones).
3. Scope — build this
Features live under Baya.Application/Features/Verification/{Commands|Queries}/<Name>/; entities in
Baya.Domain/Entities/Verification/; one IEntityTypeConfiguration<T> per entity in
Persistence/Configuration/VerificationConfig/; one EF migration for the five tables + one seed
migration for verification_step_types.
3.1 Entities + migration
nurse_verifications [CORE] — the master per-nurse record; the SOLE source of verification truth.
- Fields:
id,nurse_id(FKnurse_profiles, UNIQUE → 1:1),status(enum, see below),submitted_at,approved_at,rejected_at,suspended_at(all nullable),rejection_reason(nullable),reviewed_by_admin_id(FKusersnullable),internal_notes(nullable), audit fields, soft-delete. - Relations: 1:1 →
nurse_profiles; 1:N →verification_steps.
verification_step_types [CORE] — the admin catalog of pipeline steps (data-driven, not an enum).
- Fields:
id,code(NVARCHAR, UNIQUE, stable machine code),display_name,description,is_required(BIT),is_automated(BIT),automation_provider(NVARCHAR nullable — e.g.shahkar,identity_kyc_vendor),sort_order(int),is_active(BIT), audit fields. - SEED (via the seed migration) exactly these six stable codes:
identity_kyc(automated),shahkar_match(automated),moh_competency_license(manual today),ino_membership(manual),criminal_record(manual, time-limited),bank_account_verification(automated, couples to b3). A new regulatory step (e.g. professional-liability insurance) is one INSERT — never hardcode the six in a C# enum. - Relations: 1:N →
verification_steps.
verification_steps [CORE] — one row per step per nurse.
- Fields:
id,nurse_verification_id(FK),step_type_id(FK),status(enum, see below),external_response_json(NVARCHAR(MAX) nullable — raw KYC-vendor response for audit),expires_at(DATETIME2 nullable — for time-limited steps),is_automated(BIT — snapshotted from the step type at seed time, read this, never the live step-type),started_at/completed_at(nullable),failure_reason(nullable), audit fields.UNIQUE(nurse_verification_id, step_type_id). - On expiry → revert to
pending+RaiseSupportAlert(handled by the scanner, §3.2). - Relations: N:1 →
nurse_verifications,verification_step_types; 1:N →verification_documents.
verification_documents [CORE] — metadata only; bytes never in the DB.
- Fields:
id,step_id(FKverification_steps),object_storage_key(NVARCHAR — the opaqueIObjectStoragekey),integrity_hash(NVARCHAR(64) — content hash to detect tampering/swap),content_type,file_size_bytes(BIGINT),original_file_name(nullable),uploaded_by_user_id(FKusers), audit fields, soft-delete. - No PII bytes, ever — the file lives in object storage behind a signed URL; the DB row is metadata.
- Relations: N:1 →
verification_steps.
nurse_credentials [MVP] — the structured, queryable credential registry (powers badge + renewal).
- Fields (mirror
04-verification-and-credentials.mdexactly):id,nurse_id(FKnurse_profiles),credential_type(NVARCHAR —moh_competency_license/ino_membership/criminal_record),credential_number(NVARCHAR(100) encrypted viaIFieldEncryptor),holder_name_snapshot(NVARCHAR(200) — name as printed, for ID cross-check),issuing_authority(NVARCHAR(200)),issued_at(DATE nullable),expires_at(DATE nullable — drives renewal alerts),verification_source(NVARCHAR(300) nullable — portal URL/method),verification_method(NVARCHAR(20) —manual/portal/api),verified_by_admin_id(FKusersnullable), audit fields, soft-delete. - Relations: N:1 →
nurse_profiles; cross-referenced by the relevantverification_steps.
Status enums (define as proper enums; persist per project convention):
VerificationStatus:not_started|pending|in_review|approved|rejected|suspended.VerificationStepStatus:not_started|pending|in_review|passed|failed|expired. (pending= awaiting submission/automation;in_review= awaiting manual admin decision.)
3.2 Commands & queries (CQRS, OperationResult, never throw for expected failures)
| Capability | Type | Route | What it does |
|---|---|---|---|
AdminUpsertStepTypeCommand / AdminListStepTypesQuery / AdminDeactivateStepTypeCommand |
Cmd/Query | POST/GET/DELETE api/v1/admin_verification_step_types[/{id}] |
Admin CRUD over the step-type catalog (code immutable once used, is_required/is_automated/automation_provider/sort_order/is_active). Adding a step = one row. Cached (read-heavy reference data) with invalidation on write. |
SubmitNurseVerificationCommand |
Command | POST api/v1/nurse_verification/submit |
For the signed-in nurse (tenancy via ICurrentUser): upsert a nurse_verifications row, set status='pending' + submitted_at, and seed one verification_steps row per active required verification_step_types (status pending/not_started, snapshotting is_automated onto each step). Idempotent: re-submitting does not duplicate steps. |
GetNurseVerificationStatusQuery |
Query | GET api/v1/nurse_verification |
The nurse's aggregate status + per-step list (code, display, status, expires_at, failure reason, whether automated) + a "what's blocking bookability" summary. Projected (AsNoTracking + .Select), tenancy-scoped. Feeds f5's checklist. |
RequestDocumentUploadUrlCommand |
Command | POST api/v1/nurse_verification/steps/{stepId}/upload_url |
Returns a signed PUT URL from IObjectStorage for a manual-evidence step (MoH license PDF, INO card, عدم سوء پیشینه). Validates the step belongs to the nurse and accepts uploads. Returns the object_storage_key the client echoes back on confirm. |
ConfirmDocumentUploadCommand |
Command | POST api/v1/nurse_verification/steps/{stepId}/documents |
After the client uploads to the signed URL: persist a verification_documents metadata row (key, integrity_hash computed/echoed, content type, size). Bytes never enter the DB. Moves the step to in_review if it's a manual step. |
RunIdentityKycCommand |
Command | POST api/v1/nurse_verification/steps/identity_kyc/run |
Calls IIdentityKycProvider (national-ID validity + name match + liveness). Persists external_response_json; on pass populates users.national_id + national_id_verified_at and sets the step passed; on fail sets failed + failure_reason. Re-aggregates (§ AggregateAndFinalize). |
RunShahkarMatchCommand |
Command | POST api/v1/nurse_verification/steps/shahkar_match/run |
Calls IShahkarVerifier to bind the login SIM ↔ the verified users.national_id. Persists external_response_json + sets shahkar_verified_at on pass. Shared-SIM is an explicit handled failure state → step failed + RaiseSupportAlert(shared_sim). Requires identity KYC passed first (national-ID present). |
RunBankAccountVerificationCommand |
Command | POST api/v1/nurse_verification/steps/bank_account_verification/run |
Reuses IBankAccountOwnershipVerifier (b3) on the nurse's primary nurse_bank_accounts: holder national-ID must equal the verified nurse national-ID. On match sets the bank account's matched_national_id=1 + the step passed; on mismatch failed (money-mule guard). |
AdminListPendingStepsQuery |
Query | GET api/v1/admin_verifications?status=in_review&page=&page_size= |
The manual-review worklist (MoH/INO/criminal steps in in_review): nurse, step code, submitted docs (with signed GET URLs via IObjectStorage). Projected + paginated. |
AdminGetVerificationDetailQuery |
Query | GET api/v1/admin_verifications/{nurseVerificationId} |
Full per-nurse detail for the doc-viewer: all steps, documents (signed URLs), existing credentials, identity name (for cross-check). Projected. |
AdminReviewStepCommand |
Command | POST api/v1/admin_verifications/steps/{stepId}/decide |
Admin passes or rejects a manual step with a required reason on reject. On pass of a credential-bearing step (MoH/INO/criminal): record a nurse_credentials row (encrypted credential_number, holder_name_snapshot cross-checked against the identity name, authority, issued/expires, verification_method='manual', verified_by_admin_id). Writes an audit-log entry (b1 audit interceptor + an explicit decision record). Re-aggregates. |
AggregateAndFinalize |
Command (internal step, called after every outcome write) | — | Re-reads all required steps. Sets nurse_verifications.status (pending→in_review when any step is in_review; rejected if any required step failed; approved only when every required step is passed). On all-passed: flip nurse_profiles.is_verified = 1 in the SAME transaction, set approved_at. Idempotent — re-running an already-approved verification is a no-op. |
AdminSuspendVerificationCommand |
Command | POST api/v1/admin_verifications/{nurseVerificationId}/suspend |
Sets status='suspended' + suspended_at and reverses the flip — nurse_profiles.is_verified = 0 in the same transaction (un-publishing the nurse from search via b7's projection hook). Records reason + admin. |
ScanExpiringCredentialsCommand |
Command | POST api/v1/admin_verifications/scan_expiring (admin-triggered; cron DEFERRED) |
Scans nurse_credentials.expires_at and the matching time-limited verification_steps.expires_at (criminal-record especially). For each lapsed credential: revert the step to pending/expired, raise a support_alert + a renewal-prompt notification, and if a required credential lapsed, re-aggregate (which can drop is_verified via suspension semantics). Paginated/batched. |
GetVerifiedTrustBadgeQuery |
Query | GET api/v1/nurses/{nurseId}/trust_badge (public) |
The public "verified" badge sourced from approved nurse_verifications + non-expired nurse_credentials (credential types held, e.g. "MoH license · INO member", never the encrypted numbers). Projected, cached. Feeds the public nurse profile (f6). |
- Controllers:
NurseVerificationController(nurse policy, tenancy-scoped),AdminVerificationController(admin policy, sensitive — rate-limited), and the public trust-badge action (anonymous, read-only). Allsealed : BaseController, injectISender, returnbase.OperationResult(...), snake_case[controller]/[action]routes,CancellationTokenthreaded. - Validators: FluentValidation on the submit/run/decide/upload commands (national-ID format on identity
run; required rejection reason on a reject decision; required
expires_aton the criminal-record credential; step ownership/tenancy on the nurse-side commands).
3.3 DEFERRED (build the seam/flag, not the feature)
- The scheduled expiry-scan cron (
CredentialExpiryScannerJob, emulating Nursys e-Notify cadence) — DEFERRED. The scan logic ships now asScanExpiringCredentialsCommand, admin-triggered; leave a clean entry point + averification_expiry_scan_cadenceconfig key. (Roadmap: a hosted scheduler.) - Automated MoH/INO license lookup — DEFERRED behind
ICredentialVerifier(verification_method='api'); the manual review path is the default impl today. Build the seam, not the API client. fraud_flags/ ML fraud scoring and the professional-liability-insurance step — DEFERRED (fraud_flagsmodeled-but-inactive; the insurance step is "addable as a row when required"). Do not build.- Customer national-ID KYC — out of scope here (the column stays unused per b3). Do not gate on it.
4. Mocks & seams in this phase
| Seam | Owner | Mock behaviour | Registry |
|---|---|---|---|
IShahkarVerifier |
introduced here | MatchAsync(phone, nationalId, ct) returns a deterministic result + a fake vendor_ref + an external_response_json blob: pass when phone+national-ID match a seed map; force shared-SIM failure for a known test number (the explicit handled state). No real Shahkar call. |
add a new row (🟡) |
IIdentityKycProvider |
introduced here | VerifyAsync(nationalId, livenessPayload, ct) returns deterministic pass/fail by test national-ID + a fake vendor ref + external_response_json; pass implies a name-match string used for the credential cross-check. No real liveness/OCR. |
add a new row (🟡) |
ICredentialVerifier |
introduced here | The manual-admin default impl is the mock: Verify(credentialType, …) returns RequiresManualReview today (verification_method='manual'), shaped so an api/portal impl drops in later for MoH/INO. No portal call. |
add a new row (🟡) |
IBankAccountOwnershipVerifier |
reuse from b3 | Sheba inquiry mock; returns holder national-ID = entered one (or mismatch for a test IBAN). Used by bank_account_verification. |
reuse row |
IObjectStorage |
reuse from b0 | local-disk/in-memory blob store; signed PUT/GET URLs for verification_documents. |
reuse row |
IFieldEncryptor |
reuse from b0 | local symmetric key; encrypts credential_number, never logs plaintext. |
reuse row |
ICacheService |
reuse from b0/b1 | in-memory; behind the typed config accessor + the step-type/badge caches. | reuse row |
Each new seam is an Application-layer interface with an Infrastructure mock and DI registration
via a ServiceConfiguration/ extension (real impl is config-selected later — never an if (mock) branch
in a handler). Persist the raw vendor response in external_response_json so the audit trail survives the
swap. Append the three new rows to
../../shared-working-context/reports/mocks-registry.md
(seam, file, what's faked, config keys, step-by-step how to make it real — which Iranian KYC vendor
[Finnotech / U-ID / Jibbit / Farashensa / Verify / Kavoshak], which methods to implement, what to test; and
for ICredentialVerifier, the note that MoH/INO have no public B2B API so it stays manual until one
appears).
5. Critical rules you must not get wrong
nurse_verifications.statusis the SINGLE source of truth.nurse_profiles.verification_statusno longer exists — do not reintroduce it or any second copy of verification state. Every read of "is this nurse verified for state purposes" goes throughnurse_verifications.status; the only derived boolean isnurse_profiles.is_verified, written solely by the flip below.- Flip
nurse_profiles.is_verified = 1ONLY inside the all-passed transaction that confirms every required step ispassed(AggregateAndFinalize). Never set it from a controller, a partial pass, or an out-of-band update. Reverse it (is_verified = 0) in the same transaction on suspension — a suspended or lapsed nurse must immediately stop being bookable/searchable. - A nurse is bookable only after all required steps pass. Any failing/pending/expired required step keeps
every service variant unbookable. This is the gate b7's
nurse_search_index.is_searchablereads. - The pipeline is data-driven — read required steps from
verification_step_types, never a code enum. A new step is an INSERT. Seed the six codes; don't branch on them in aswitchthat would break when a row is added. step_type.is_automatedis SNAPSHOTTED ontoverification_stepsat seed time — read the snapshot, not the live step type. Historical records must survive later catalog edits (an admin toggling a step's automation must not rewrite the meaning of past verifications).- Expiring credentials auto-revert the step. On
expires_atlapse (criminal-record especially), revert the step topending/expired, raise asupport_alert, send a renewal prompt, and if a required credential lapsed, re-gate bookability. A lapsed certificate must never silently keep a nurse verified. users.national_idis populated only after the identity step passes, and every downstream comparison (Shahkar match, IBAN ownership, credential holder-name cross-check) compares against that verified national-ID. An unverified registration must never look KYC-complete.- IBAN ownership gates the first payout, not admin eyeballing.
bank_account_verificationreuses b3'sIBankAccountOwnershipVerifier; the holder national-ID must equal the verified nurse national-ID (money-mule prevention). Never pass the step on a mismatch. verification_documentsstore metadata only — bytes live in object storage behind signed URLs, with an integrity hash; files are never public. No document byte stream is ever written to or returned from the DB; access is always a short-lived signed URL.credential_numberis encrypted PII (throughIFieldEncryptor) andholder_name_snapshotmust be cross-checked against the nurse's identity name before a credential is recorded as verified. Never trust an uploaded file alone — forgery (the "imposter nurse") is the documented attack.- Shared-SIM is an explicit handled state, not an undefined edge: fail Shahkar gracefully with a clear,
non-accusatory reason and raise a
support_alert. Re-run Shahkar on phone change (shahkar_verified_atresets to NULL upstream). - Every manual admin decision is auditable (the b1 audit interceptor + an explicit decision record) for defensibility — verification is platform-owned and must never be marketed as a check not actually performed.
- All vendor steps are mocked now — but behind real DI seams. No mock behaviour baked into call sites; the swap is implementation-only and config-selected.
- Tenancy: every nurse-side command/query is scoped to the authenticated nurse via
ICurrentUser; a nurse can never read or act on another nurse's verification. Admin endpoints sit behind the admin policy and are rate-limited. The public trust badge exposes credential types held, never numbers.
6. Definition of Done
The shared definition-of-done.md, plus:
- The five tables (
nurse_verifications,verification_step_types,verification_steps,verification_documents,nurse_credentials) exist via one migration, each with itsIEntityTypeConfiguration<T>, thenurse_idUNIQUE (1:1) onnurse_verifications, theUNIQUE(nurse_verification_id, step_type_id)onverification_steps, the snapshotis_automatedcolumn, encryptedcredential_number, and soft-delete/audit wiring. - The seed migration loads the six
verification_step_typeswith the exact stable codes and the correctis_required/is_automatedflags. - All §3.2 commands/queries implemented (CQRS,
OperationResult, projected + paginated reads, validators), withNurseVerificationController,AdminVerificationController, and the public badge. IShahkarVerifier,IIdentityKycProvider,ICredentialVerifierintroduced (Application interfaces, Infrastructure mocks, DI via aServiceConfiguration/extension, config-selected); the b3IBankAccountOwnershipVerifierand b0IObjectStorage/IFieldEncryptorseams reused (not redefined). Noif (mock)in handlers.- The flip transaction is correct:
is_verified=1only on all-required-passed, in one transaction;is_verified=0on suspension, in one transaction; the expiry scan reverts steps + raises alerts. - Handler unit tests (NSubstitute) for: step seeding from required step-types, the snapshot of
is_automated, the automated-step pass/fail (Shahkar + identity KYC mocks), the manual-review → credential-record path with holder-name cross-check, the all-passed flip, the suspension reverse, and the expiry revert + alert. ≥1WebApplicationFactoryintegration test per controller (happy path, 401, validation 400).dotnet build Baya.slnzero new warnings;dotnet test Baya.slngreen. - The
Baya.Application/Features/Verification/**area is added to the Project map inserver/CLAUDE.md; the three new seams noted where seams are documented. - The contract
dev/contracts/domains/verification.mdis written and theswagger.jsonsnapshot republished.
7. How to test (what a human can verify after this phase)
Prereqs: a nurse user with a nurse_profiles row (b3) and (for the bank step) a primary
nurse_bank_accounts row; an admin user. The seam mocks are deterministic — use the seed/test national-IDs.
- Seed step-types —
GET api/v1/admin_verification_step_types→ the six seeded codes appear with correctis_required/is_automatedflags. Add a 7th viaPOST→ it persists (proves data-driven). - Submit — as the nurse,
POST api/v1/nurse_verification/submit→ anurse_verificationsrow inpendingand oneverification_stepsrow per required step, each withis_automatedsnapshotted.GET api/v1/nurse_verificationshows the checklist + "what's blocking bookability". - Upload a document —
POST …/steps/{stepId}/upload_urlreturns a signed URL; PUT a file to it (mock storage);POST …/steps/{stepId}/documents→ averification_documentsmetadata row with theobject_storage_key+integrity_hashstored; no bytes in the DB; the manual step moves toin_review. - Run automated Shahkar / identity KYC —
POST …/steps/identity_kyc/run(pass test national-ID) → steppassed,users.national_idpopulated,external_response_jsonstored; thenPOST …/steps/shahkar_match/run→passed(andshahkar_verified_atset). Run Shahkar with the shared-SIM test number → stepfailed+ asupport_alertraised. - Run bank-account verification —
POST …/steps/bank_account_verification/run(matching test IBAN) →passed+ the account'smatched_national_id=1; with the mismatch test IBAN →failed. - Admin approves manual steps —
GET api/v1/admin_verifications?status=in_reviewshows the queue;POST api/v1/admin_verifications/steps/{stepId}/decide(pass) on MoH + INO + criminal-record → each records anurse_credentialsrow (encrypted number, holder-name cross-check, expiry on the criminal record). Reject one with a reason → stepfailedand the nurse sees the reason. - All steps passed flips
is_verifiedin one transaction — once every required step ispassed,nurse_verifications.status='approved'andnurse_profiles.is_verified=1— confirm both changed together (verify there is no in-between state where the verification is approved butis_verifiedis still 0). - Suspend reverses it —
POST api/v1/admin_verifications/{id}/suspend→status='suspended'andis_verified=0together. - Expire a credential — set a
nurse_credentials.expires_at(criminal record) in the past, runPOST api/v1/admin_verifications/scan_expiring→ the matching step reverts topending/expired, asupport_alertis raised, and (since it's required) the nurse is re-gated. - Public trust badge —
GET api/v1/nurses/{nurseId}/trust_badge→ shows the verified status + credential types held; no credential numbers are ever returned.
8. Hand off & document (close the phase)
- Docs to update: the Project map in
server/CLAUDE.md(add theFeatures/Verification/**area + the three new seams). If you discover/confirm a rule the product docs don't capture (e.g. the exactin_reviewvspendingstep semantics, the renewal-prompt notification, or the trust-badge "types not numbers" rule), record it inproduct/business/02-nurse-verification.md/product/data-model/04-verification-and-credentials.md— don't invent rules. Note the new seam family inserver/CONVENTIONS.mdif it establishes a reusable pattern. - Contract to write:
dev/contracts/domains/verification.md(per../../contracts/domains/_TEMPLATE.md) — the nurse endpoints (submit, status, upload-url, confirm-document, run identity/Shahkar/bank steps), the admin endpoints (step-type CRUD, pending-steps queue, detail, decide, suspend, scan-expiring), and the public trust-badge endpoint; theVerificationStatus/VerificationStepStatusenums and the six step-typecodes; the step / step-type / document (signed-URL) / credential DTO shapes (credential_numbernever serialized; badge shows types not numbers); auth/tenancy/rate-limit notes; the side effects (is_verifiedflip,support_alertsraised,users.national_idpopulated). Republish theswagger.jsonsnapshot per../../contracts/openapi/README.md. This is what f5-b6 consumes. - Handoff & report: write
dev/shared-working-context/backend/handoff/after-backend-phase-6.md(the verification pipeline is live, what f5 can now build — the nurse verification checklist, identity submit, document upload, credentials form, under-review state, trust badge; which endpoints/contracts are live; that all vendors are mocked behindIShahkarVerifier/IIdentityKycProvider/ICredentialVerifierand the reused b3 bank seam). Append tobackend/STATUS.md, writedev/shared-working-context/reports/backend-phase-6-report.md(what was built, what is now testable and exactly how per §7, what is mocked + how to make it real, contracts produced, follow-ups: the expiry-scan cron, automated MoH/INO lookup,fraud_flags), and updatedev/shared-working-context/reports/mocks-registry.md(the three new seam rows → 🟡; the reused rows noted). - Memory: save a
projectmemory note for the non-obvious decisions this phase fixes —statusis the single source of truth (noverification_statuscopy), the guardedis_verifiedflip/reverse transaction, theis_automatedsnapshot rule, the data-driven step catalog, the expiry-revert + alert flow, documents-as- metadata-only, and the three new vendor seams — with a one-line pointer inMEMORY.md.