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>
This commit is contained in:
hamid
2026-07-05 14:39:32 +03:30
parent 687fbfc6d9
commit 1c266523bc
105 changed files with 13938 additions and 10 deletions
@@ -12,6 +12,38 @@ public sealed class SeamOptions
public ObjectStorageOptions ObjectStorage { get; set; } = new();
public BankOwnershipOptions BankOwnership { get; set; } = new();
public GeocodingOptions Geocoding { get; set; } = new();
public ShahkarOptions Shahkar { get; set; } = new();
public IdentityKycOptions IdentityKyc { get; set; } = new();
}
/// <summary>
/// Tunes the mock <c>IShahkarVerifier</c> (phone↔national-id binding). A submitted phone equal to
/// <see cref="SharedSimPhone"/> returns the explicit shared-SIM failure; a national id equal to
/// <see cref="MismatchNationalId"/> returns a plain mismatch; every other pair matches. The real vendor
/// implementation ignores these.
/// </summary>
public sealed class ShahkarOptions
{
/// <summary>The designated test phone that returns the shared-SIM failure state.</summary>
public string SharedSimPhone { get; set; } = "09120000000";
/// <summary>The designated test national id that returns a plain phone↔national-id mismatch.</summary>
public string MismatchNationalId { get; set; } = "1111111111";
}
/// <summary>
/// Tunes the mock <c>IIdentityKycProvider</c>. A national id equal to <see cref="FailNationalId"/> fails
/// KYC; every other (well-formed) national id passes with <see cref="MatchedName"/>. The real e-KYC vendor
/// implementation ignores these.
/// </summary>
public sealed class IdentityKycOptions
{
/// <summary>The designated test national id that fails identity KYC.</summary>
public string FailNationalId { get; set; } = "0000000000";
/// <summary>The name the mock reports as matched on a passing KYC (informational; the authoritative
/// identity name for credential cross-check comes from the users row).</summary>
public string MatchedName { get; set; } = "Verified Nurse";
}
/// <summary>