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:
+3346
File diff suppressed because it is too large
Load Diff
+302
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3423
File diff suppressed because it is too large
Load Diff
+84
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+493
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user