[CLAUDE] Infra: Plan B Chunk A2 — Mig 32 Contract V2 schema + Configuration + Seed sample workflow

Cookie-cutter mirror PE Mig 23+24 GỘP thành 1 Mig 32 (ADD 2 column +
FK + IX). Mirror Mig 26 pattern cho FK Restrict.

Files added/modified:
- Migrations/20260522051059_AddApprovalWorkflowToContract.cs (3-file rule )
- Migrations/20260522051059_AddApprovalWorkflowToContract.Designer.cs
- Migrations/ApplicationDbContextModelSnapshot.cs (updated)
- Configurations/ContractConfiguration.cs (+HasIndex + FK Restrict ApprovalWorkflows)
- Persistence/DbInitializer.cs (SeedSampleContractWorkflowV2 idempotent QT-HD-V2-001)

Mig 32 Up():
- ADD COLUMN Contracts.ApprovalWorkflowId Guid? NULL
- ADD COLUMN Contracts.CurrentApprovalLevelOrder int? NULL
- ADD INDEX IX_Contracts_ApprovalWorkflowId (filtered NOT NULL)
- ADD FK FK_Contracts_ApprovalWorkflows_ApprovalWorkflowId Restrict

Seed sample workflow (UAT smoke + admin Designer default):
- Code: QT-HD-V2-001 Name: "Quy trình duyệt HĐ mẫu UAT V2"
- ApplicableType: 3 (Contract) IsActive: true IsUserSelectable: true
- 1 Step "Bước 1 - Phòng CCM" + 1 Level + Approver Lê Văn Bình CCM
- Idempotent: skip nếu Code+Version existing

V1 coexist: 7 prod contract giữ WorkflowDefinitionId; V2 mới pin
ApprovalWorkflowId. Service ApproveV2Async (Chunk B em main) sẽ branch.

Verify (Implementer):
- dotnet build SolutionErp.slnx PASS 0 err (em main WIP stashed for verify)
- dotnet ef database update Dev PASS (Mig 32 applied)
- 3-file rule Mig: mig.cs + Designer.cs + Snapshot.cs

Plan B chain (6 chunks):
- A1 58898e8 Contract +2 fields (em main, done)
- A2 (this) Mig 32 schema + Config + Seed (Implementer Case 2, done)
- B Service ApproveV2Async branch (em main, in progress)
- C Mig 33 ContractLevelOpinions (Implementer, pending)
- D FE Workspace V2 (Implementer, pending)
- E FE Section 5 V2 (Implementer, pending)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-22 12:15:11 +07:00
parent 58898e8fbe
commit a85e437478
5 changed files with 4094 additions and 1 deletions

View File

@ -28,6 +28,15 @@ public class ContractConfiguration : IEntityTypeConfiguration<Contract>
b.HasIndex(x => x.ProjectId);
b.HasIndex(x => x.SlaDeadline);
b.HasIndex(x => x.BudgetId);
b.HasIndex(x => x.ApprovalWorkflowId);
// FK ApprovalWorkflowId Restrict (Plan B Chunk A2 — Mig 32 mirror PE Mig 23)
// ApprovalWorkflowsV2 pin lúc create HĐ V2. Restrict để KHÔNG xóa workflow
// nếu còn HĐ pin.
b.HasOne<SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow>()
.WithMany()
.HasForeignKey(x => x.ApprovalWorkflowId)
.OnDelete(DeleteBehavior.Restrict);
b.HasMany(x => x.Approvals).WithOne(a => a.Contract).HasForeignKey(a => a.ContractId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Comments).WithOne(c => c.Contract).HasForeignKey(c => c.ContractId).OnDelete(DeleteBehavior.Cascade);

View File

@ -68,6 +68,7 @@ public static class DbInitializer
// - SeedDemoContractsAsync ([DEMO] HĐ 7-type sample)
// - SeedDemoPurchaseEvaluationsAsync ([DEMO] PE 4 sample)
// - SeedSampleApprovalWorkflowsV2Async (V2 sample mẫu UAT cho type B)
// - SeedSampleContractWorkflowV2Async (V2 sample mẫu UAT cho Contract — Mig 32 Plan B Chunk A2)
// GIỮ: SeedRoles, SeedAdmin, SeedDepartments, SeedDemoUsers (30 user UAT),
// SeedMenuTree, SeedAdminPermissions, SeedDemoMasterData (Supplier/Project
// master), SeedContractTemplates (file template), SeedCatalogs, backfill
@ -76,7 +77,7 @@ public static class DbInitializer
var config = sp.GetRequiredService<IConfiguration>();
var demoSeedDisabled = config.GetValue<bool>("DemoSeed:Disabled");
if (demoSeedDisabled)
logger.LogInformation("DemoSeed:Disabled=true — skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10)");
logger.LogInformation("DemoSeed:Disabled=true — skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10 + Plan B Chunk A2 Contract V2)");
await SeedRolesAsync(roleManager, logger);
// Phase 6 rebrand: rename user email @solutionerp.local → @solutions.com.vn
@ -106,6 +107,7 @@ public static class DbInitializer
await SeedDemoContractsAsync(db, userManager, codeGen, logger);
await SeedDemoPurchaseEvaluationsAsync(db, userManager, logger);
await SeedSampleApprovalWorkflowsV2Async(db, userManager, logger);
await SeedSampleContractWorkflowV2Async(db, userManager, logger);
}
await WarnDefaultAdminPasswordAsync(userManager, logger);
@ -163,6 +165,58 @@ public static class DbInitializer
logger.LogInformation("Seeded sample ApprovalWorkflow V2 for DuyetNccPhuongAn: QT-DN-PA-V2-001 v01");
}
// [Plan B S29 2026-05-22 Chunk A2] Seed sample workflow V2 cho ApplicableType=Contract,
// giúp UAT HĐ V2 nhanh không cần admin tạo qua Designer trước. Idempotent — skip
// nếu đã có ANY workflow Contract (admin đã tạo) HOẶC nếu thiếu user CCM seed.
// Mirror SeedSampleApprovalWorkflowsV2Async pattern (DuyetNccPhuongAn).
private static async Task SeedSampleContractWorkflowV2Async(
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
{
var hasAnyContract = await db.ApprovalWorkflows
.AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.Contract);
if (hasAnyContract) return;
var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
if (approver is null)
{
logger.LogWarning("SeedSampleContractWorkflowV2Async: skip — approver binh.le@solutions.com.vn (Lê Văn Bình CCM) not found");
return;
}
var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");
var wf = new ApprovalWorkflow
{
Code = "QT-HD-V2-001",
Version = 1,
ApplicableType = ApprovalWorkflowApplicableType.Contract,
Name = "Quy trình duyệt HĐ mẫu UAT V2",
Description = "Sample seed cho UAT HĐ V2 — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
IsActive = true,
IsUserSelectable = true, // Mig 25 — user pick qua Workspace dropdown
ActivatedAt = DateTime.UtcNow,
};
var step = new ApprovalWorkflowStep
{
ApprovalWorkflow = wf,
Order = 1,
Name = "Bước 1 - Phòng CCM",
DepartmentId = ccmDept?.Id,
};
var level = new ApprovalWorkflowLevel
{
Step = step,
Order = 1,
Name = "Cấp 1",
ApproverUserId = approver.Id,
};
wf.Steps.Add(step);
step.Levels.Add(level);
db.ApprovalWorkflows.Add(wf);
await db.SaveChangesAsync();
logger.LogInformation("Seeded sample ApprovalWorkflow V2 for Contract: QT-HD-V2-001 v01");
}
// Seed 4 master catalogs với defaults cho user nhập liệu Details. Idempotent:
// skip per-table nếu đã có row (admin có thể đã thêm/sửa — không clobber).
private static async Task SeedCatalogsAsync(ApplicationDbContext db, ILogger logger)

View File

@ -0,0 +1,60 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddApprovalWorkflowToContract : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ApprovalWorkflowId",
table: "Contracts",
type: "uniqueidentifier",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "CurrentApprovalLevelOrder",
table: "Contracts",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Contracts_ApprovalWorkflowId",
table: "Contracts",
column: "ApprovalWorkflowId");
migrationBuilder.AddForeignKey(
name: "FK_Contracts_ApprovalWorkflows_ApprovalWorkflowId",
table: "Contracts",
column: "ApprovalWorkflowId",
principalTable: "ApprovalWorkflows",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Contracts_ApprovalWorkflows_ApprovalWorkflowId",
table: "Contracts");
migrationBuilder.DropIndex(
name: "IX_Contracts_ApprovalWorkflowId",
table: "Contracts");
migrationBuilder.DropColumn(
name: "ApprovalWorkflowId",
table: "Contracts");
migrationBuilder.DropColumn(
name: "CurrentApprovalLevelOrder",
table: "Contracts");
}
}
}

View File

@ -637,6 +637,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("ApprovalWorkflowId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("BudgetId")
.HasColumnType("uniqueidentifier");
@ -657,6 +660,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int?>("CurrentApprovalLevelOrder")
.HasColumnType("int");
b.Property<int?>("CurrentWorkflowStepIndex")
.HasColumnType("int");
@ -732,6 +738,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("ApprovalWorkflowId");
b.HasIndex("BudgetId");
b.HasIndex("MaHopDong")
@ -3479,6 +3487,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Budget");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
{
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
.WithMany()
.HasForeignKey("ApprovalWorkflowId")
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b =>
{
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")