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
@@ -0,0 +1,302 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Baya.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class VerificationPipeline : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "verif");
migrationBuilder.CreateTable(
name: "NurseCredentials",
schema: "verif",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
NurseId = table.Column<long>(type: "bigint", nullable: false),
CredentialType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
CredentialNumber = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
HolderNameSnapshot = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IssuingAuthority = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IssuedAt = table.Column<DateOnly>(type: "date", nullable: true),
ExpiresAt = table.Column<DateOnly>(type: "date", nullable: true),
VerificationSource = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
VerificationMethod = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
VerifiedByAdminId = table.Column<int>(type: "int", nullable: true),
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedById = table.Column<int>(type: "int", nullable: true),
ModifiedById = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NurseCredentials", x => x.Id);
table.ForeignKey(
name: "FK_NurseCredentials_NurseProfiles_NurseId",
column: x => x.NurseId,
principalSchema: "usr",
principalTable: "NurseProfiles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_NurseCredentials_Users_VerifiedByAdminId",
column: x => x.VerifiedByAdminId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.CreateTable(
name: "NurseVerifications",
schema: "verif",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
NurseId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
SubmittedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
ApprovedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
RejectedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
SuspendedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
RejectionReason = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
ReviewedByAdminId = table.Column<int>(type: "int", nullable: true),
InternalNotes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedById = table.Column<int>(type: "int", nullable: true),
ModifiedById = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NurseVerifications", x => x.Id);
table.ForeignKey(
name: "FK_NurseVerifications_NurseProfiles_NurseId",
column: x => x.NurseId,
principalSchema: "usr",
principalTable: "NurseProfiles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_NurseVerifications_Users_ReviewedByAdminId",
column: x => x.ReviewedByAdminId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.CreateTable(
name: "VerificationStepTypes",
schema: "verif",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IsRequired = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsAutomated = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
AutomationProvider = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedById = table.Column<int>(type: "int", nullable: true),
ModifiedById = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_VerificationStepTypes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "VerificationSteps",
schema: "verif",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
NurseVerificationId = table.Column<long>(type: "bigint", nullable: false),
StepTypeId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
ExternalResponseJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
ExpiresAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
IsAutomated = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
StartedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CompletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
FailureReason = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedById = table.Column<int>(type: "int", nullable: true),
ModifiedById = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_VerificationSteps", x => x.Id);
table.ForeignKey(
name: "FK_VerificationSteps_NurseVerifications_NurseVerificationId",
column: x => x.NurseVerificationId,
principalSchema: "verif",
principalTable: "NurseVerifications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_VerificationSteps_VerificationStepTypes_StepTypeId",
column: x => x.StepTypeId,
principalSchema: "verif",
principalTable: "VerificationStepTypes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "VerificationDocuments",
schema: "verif",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
StepId = table.Column<long>(type: "bigint", nullable: false),
ObjectStorageKey = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: false),
IntegrityHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
FileSizeBytes = table.Column<long>(type: "bigint", nullable: false),
OriginalFileName = table.Column<string>(type: "nvarchar(260)", maxLength: 260, nullable: true),
UploadedByUserId = table.Column<int>(type: "int", nullable: false),
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedById = table.Column<int>(type: "int", nullable: true),
ModifiedById = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_VerificationDocuments", x => x.Id);
table.ForeignKey(
name: "FK_VerificationDocuments_Users_UploadedByUserId",
column: x => x.UploadedByUserId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_VerificationDocuments_VerificationSteps_StepId",
column: x => x.StepId,
principalSchema: "verif",
principalTable: "VerificationSteps",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.InsertData(
schema: "ops",
table: "PlatformConfigs",
columns: new[] { "Id", "CreatedAt", "CreatedById", "DataType", "Description", "Key", "ModifiedAt", "ModifiedById", "Value" },
values: new object[] { 16L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).", "verification_expiry_scan_cadence_hours", null, null, "24" });
migrationBuilder.CreateIndex(
name: "IX_NurseCredentials_NurseId_CredentialType",
schema: "verif",
table: "NurseCredentials",
columns: new[] { "NurseId", "CredentialType" });
migrationBuilder.CreateIndex(
name: "IX_NurseCredentials_VerifiedByAdminId",
schema: "verif",
table: "NurseCredentials",
column: "VerifiedByAdminId");
migrationBuilder.CreateIndex(
name: "IX_NurseVerifications_NurseId",
schema: "verif",
table: "NurseVerifications",
column: "NurseId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_NurseVerifications_ReviewedByAdminId",
schema: "verif",
table: "NurseVerifications",
column: "ReviewedByAdminId");
migrationBuilder.CreateIndex(
name: "IX_VerificationDocuments_StepId",
schema: "verif",
table: "VerificationDocuments",
column: "StepId");
migrationBuilder.CreateIndex(
name: "IX_VerificationDocuments_UploadedByUserId",
schema: "verif",
table: "VerificationDocuments",
column: "UploadedByUserId");
migrationBuilder.CreateIndex(
name: "IX_VerificationSteps_StepTypeId",
schema: "verif",
table: "VerificationSteps",
column: "StepTypeId");
migrationBuilder.CreateIndex(
name: "UX_VerificationSteps_Verification_StepType",
schema: "verif",
table: "VerificationSteps",
columns: new[] { "NurseVerificationId", "StepTypeId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VerificationStepTypes_Code",
schema: "verif",
table: "VerificationStepTypes",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VerificationStepTypes_IsActive_SortOrder",
schema: "verif",
table: "VerificationStepTypes",
columns: new[] { "IsActive", "SortOrder" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NurseCredentials",
schema: "verif");
migrationBuilder.DropTable(
name: "VerificationDocuments",
schema: "verif");
migrationBuilder.DropTable(
name: "VerificationSteps",
schema: "verif");
migrationBuilder.DropTable(
name: "NurseVerifications",
schema: "verif");
migrationBuilder.DropTable(
name: "VerificationStepTypes",
schema: "verif");
migrationBuilder.DeleteData(
schema: "ops",
table: "PlatformConfigs",
keyColumn: "Id",
keyValue: 16L);
}
}
}
@@ -0,0 +1,84 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Baya.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class SeedVerificationStepTypes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
schema: "verif",
table: "VerificationStepTypes",
columns: new[] { "Id", "AutomationProvider", "Code", "CreatedAt", "CreatedById", "Description", "DisplayName", "IsActive", "IsAutomated", "IsRequired", "ModifiedAt", "ModifiedById", "SortOrder" },
values: new object[,]
{
{ 1L, "identity_kyc_vendor", "identity_kyc", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "National-ID validity, name match and photo/video liveness via an Iranian e-KYC vendor.", "Identity Verification (KYC)", true, true, true, null, null, 1 },
{ 2L, "shahkar", "shahkar_match", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "Confirms the login SIM is registered to the nurse's own national ID (شاهکار).", "Shahkar Phone Binding", true, true, true, null, null, 2 }
});
migrationBuilder.InsertData(
schema: "verif",
table: "VerificationStepTypes",
columns: new[] { "Id", "AutomationProvider", "Code", "CreatedAt", "CreatedById", "Description", "DisplayName", "IsActive", "IsRequired", "ModifiedAt", "ModifiedById", "SortOrder" },
values: new object[,]
{
{ 3L, null, "moh_competency_license", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "پروانه صلاحیت حرفه‌ای — the MoH-mandated in-home nursing licence (bundles the criminal-record screen). Manual today.", "MoH Professional Competency License", true, true, null, null, 3 },
{ 4L, null, "ino_membership", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "نظام پرستاری membership cross-check (ino.ir). Manual today.", "Nursing Organization (INO) Membership", true, true, null, null, 4 },
{ 5L, null, "criminal_record", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "عدم سوء پیشینه — consent-gated, nurse-uploaded, time-limited (reverts on expiry).", "Criminal Record Certificate", true, true, null, null, 5 }
});
migrationBuilder.InsertData(
schema: "verif",
table: "VerificationStepTypes",
columns: new[] { "Id", "AutomationProvider", "Code", "CreatedAt", "CreatedById", "Description", "DisplayName", "IsActive", "IsAutomated", "IsRequired", "ModifiedAt", "ModifiedById", "SortOrder" },
values: new object[] { 6L, "sheba", "bank_account_verification", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "استعلام شبا — the payout IBAN owner's national ID must equal the verified nurse national ID.", "Bank Account (IBAN) Ownership", true, true, true, null, null, 6 });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 1L);
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 2L);
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 3L);
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 4L);
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 5L);
migrationBuilder.DeleteData(
schema: "verif",
table: "VerificationStepTypes",
keyColumn: "Id",
keyValue: 6L);
}
}
}
@@ -609,6 +609,15 @@ namespace Baya.Infrastructure.Persistence.Migrations
Description = "Refresh-token session lifetime in days.",
Key = "auth_session_ttl_days",
Value = "30"
},
new
{
Id = 16L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "int",
Description = "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).",
Key = "verification_expiry_scan_cadence_hours",
Value = "24"
});
});
@@ -2560,6 +2569,411 @@ namespace Baya.Infrastructure.Persistence.Migrations
b.ToTable("UserTokens", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<string>("CredentialNumber")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("CredentialType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTimeOffset?>("DeletedAt")
.HasColumnType("datetimeoffset");
b.Property<DateOnly?>("ExpiresAt")
.HasColumnType("date");
b.Property<string>("HolderNameSnapshot")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateOnly?>("IssuedAt")
.HasColumnType("date");
b.Property<string>("IssuingAuthority")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<long>("NurseId")
.HasColumnType("bigint");
b.Property<string>("VerificationMethod")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("VerificationSource")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<int?>("VerifiedByAdminId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("VerifiedByAdminId");
b.HasIndex("NurseId", "CredentialType");
b.ToTable("NurseCredentials", "verif");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset?>("ApprovedAt")
.HasColumnType("datetimeoffset");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<DateTimeOffset?>("DeletedAt")
.HasColumnType("datetimeoffset");
b.Property<string>("InternalNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<long>("NurseId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("RejectedAt")
.HasColumnType("datetimeoffset");
b.Property<string>("RejectionReason")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int?>("ReviewedByAdminId")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTimeOffset?>("SubmittedAt")
.HasColumnType("datetimeoffset");
b.Property<DateTimeOffset?>("SuspendedAt")
.HasColumnType("datetimeoffset");
b.HasKey("Id");
b.HasIndex("NurseId")
.IsUnique();
b.HasIndex("ReviewedByAdminId");
b.ToTable("NurseVerifications", "verif");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<DateTimeOffset?>("DeletedAt")
.HasColumnType("datetimeoffset");
b.Property<long>("FileSizeBytes")
.HasColumnType("bigint");
b.Property<string>("IntegrityHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<string>("ObjectStorageKey")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("OriginalFileName")
.HasMaxLength(260)
.HasColumnType("nvarchar(260)");
b.Property<long>("StepId")
.HasColumnType("bigint");
b.Property<int>("UploadedByUserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("StepId");
b.HasIndex("UploadedByUserId");
b.ToTable("VerificationDocuments", "verif");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset?>("CompletedAt")
.HasColumnType("datetimeoffset");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<DateTimeOffset?>("ExpiresAt")
.HasColumnType("datetimeoffset");
b.Property<string>("ExternalResponseJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("FailureReason")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsAutomated")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<long>("NurseVerificationId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("StartedAt")
.HasColumnType("datetimeoffset");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<long>("StepTypeId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("StepTypeId");
b.HasIndex("NurseVerificationId", "StepTypeId")
.IsUnique()
.HasDatabaseName("UX_VerificationSteps_Verification_StepType");
b.ToTable("VerificationSteps", "verif");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AutomationProvider")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsAutomated")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsRequired")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("IsActive", "SortOrder");
b.ToTable("VerificationStepTypes", "verif");
b.HasData(
new
{
Id = 1L,
AutomationProvider = "identity_kyc_vendor",
Code = "identity_kyc",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "National-ID validity, name match and photo/video liveness via an Iranian e-KYC vendor.",
DisplayName = "Identity Verification (KYC)",
IsActive = true,
IsAutomated = true,
IsRequired = true,
SortOrder = 1
},
new
{
Id = 2L,
AutomationProvider = "shahkar",
Code = "shahkar_match",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "Confirms the login SIM is registered to the nurse's own national ID (شاهکار).",
DisplayName = "Shahkar Phone Binding",
IsActive = true,
IsAutomated = true,
IsRequired = true,
SortOrder = 2
},
new
{
Id = 3L,
Code = "moh_competency_license",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "پروانه صلاحیت حرفه‌ای — the MoH-mandated in-home nursing licence (bundles the criminal-record screen). Manual today.",
DisplayName = "MoH Professional Competency License",
IsActive = true,
IsAutomated = false,
IsRequired = true,
SortOrder = 3
},
new
{
Id = 4L,
Code = "ino_membership",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "نظام پرستاری membership cross-check (ino.ir). Manual today.",
DisplayName = "Nursing Organization (INO) Membership",
IsActive = true,
IsAutomated = false,
IsRequired = true,
SortOrder = 4
},
new
{
Id = 5L,
Code = "criminal_record",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "عدم سوء پیشینه — consent-gated, nurse-uploaded, time-limited (reverts on expiry).",
DisplayName = "Criminal Record Certificate",
IsActive = true,
IsAutomated = false,
IsRequired = true,
SortOrder = 5
},
new
{
Id = 6L,
AutomationProvider = "sheba",
Code = "bank_account_verification",
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
Description = "استعلام شبا — the payout IBAN owner's national ID must equal the verified nurse national ID.",
DisplayName = "Bank Account (IBAN) Ownership",
IsActive = true,
IsAutomated = true,
IsRequired = true,
SortOrder = 6
});
});
modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
@@ -2863,6 +3277,70 @@ namespace Baya.Infrastructure.Persistence.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b =>
{
b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null)
.WithMany()
.HasForeignKey("NurseId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("VerifiedByAdminId");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b =>
{
b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse")
.WithMany()
.HasForeignKey("NurseId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("ReviewedByAdminId");
b.Navigation("Nurse");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b =>
{
b.HasOne("Baya.Domain.Entities.Verification.VerificationStep", "Step")
.WithMany("Documents")
.HasForeignKey("StepId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("UploadedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Step");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b =>
{
b.HasOne("Baya.Domain.Entities.Verification.NurseVerification", "NurseVerification")
.WithMany("Steps")
.HasForeignKey("NurseVerificationId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Baya.Domain.Entities.Verification.VerificationStepType", "StepType")
.WithMany("Steps")
.HasForeignKey("StepTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("NurseVerification");
b.Navigation("StepType");
});
modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b =>
{
b.Navigation("Options");
@@ -2921,6 +3399,21 @@ namespace Baya.Infrastructure.Persistence.Migrations
b.Navigation("UserRoles");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b =>
{
b.Navigation("Steps");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b =>
{
b.Navigation("Documents");
});
modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b =>
{
b.Navigation("Steps");
});
#pragma warning restore 612, 618
}
}