[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:
@ -28,6 +28,15 @@ public class ContractConfiguration : IEntityTypeConfiguration<Contract>
|
|||||||
b.HasIndex(x => x.ProjectId);
|
b.HasIndex(x => x.ProjectId);
|
||||||
b.HasIndex(x => x.SlaDeadline);
|
b.HasIndex(x => x.SlaDeadline);
|
||||||
b.HasIndex(x => x.BudgetId);
|
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.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);
|
b.HasMany(x => x.Comments).WithOne(c => c.Contract).HasForeignKey(c => c.ContractId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|||||||
@ -68,6 +68,7 @@ public static class DbInitializer
|
|||||||
// - SeedDemoContractsAsync ([DEMO] HĐ 7-type sample)
|
// - SeedDemoContractsAsync ([DEMO] HĐ 7-type sample)
|
||||||
// - SeedDemoPurchaseEvaluationsAsync ([DEMO] PE 4 sample)
|
// - SeedDemoPurchaseEvaluationsAsync ([DEMO] PE 4 sample)
|
||||||
// - SeedSampleApprovalWorkflowsV2Async (V2 sample mẫu UAT cho type B)
|
// - 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),
|
// GIỮ: SeedRoles, SeedAdmin, SeedDepartments, SeedDemoUsers (30 user UAT),
|
||||||
// SeedMenuTree, SeedAdminPermissions, SeedDemoMasterData (Supplier/Project
|
// SeedMenuTree, SeedAdminPermissions, SeedDemoMasterData (Supplier/Project
|
||||||
// master), SeedContractTemplates (file template), SeedCatalogs, backfill
|
// master), SeedContractTemplates (file template), SeedCatalogs, backfill
|
||||||
@ -76,7 +77,7 @@ public static class DbInitializer
|
|||||||
var config = sp.GetRequiredService<IConfiguration>();
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
var demoSeedDisabled = config.GetValue<bool>("DemoSeed:Disabled");
|
var demoSeedDisabled = config.GetValue<bool>("DemoSeed:Disabled");
|
||||||
if (demoSeedDisabled)
|
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);
|
await SeedRolesAsync(roleManager, logger);
|
||||||
// Phase 6 rebrand: rename user email @solutionerp.local → @solutions.com.vn
|
// 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 SeedDemoContractsAsync(db, userManager, codeGen, logger);
|
||||||
await SeedDemoPurchaseEvaluationsAsync(db, userManager, logger);
|
await SeedDemoPurchaseEvaluationsAsync(db, userManager, logger);
|
||||||
await SeedSampleApprovalWorkflowsV2Async(db, userManager, logger);
|
await SeedSampleApprovalWorkflowsV2Async(db, userManager, logger);
|
||||||
|
await SeedSampleContractWorkflowV2Async(db, userManager, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
await WarnDefaultAdminPasswordAsync(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");
|
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:
|
// 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).
|
// 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)
|
private static async Task SeedCatalogsAsync(ApplicationDbContext db, ILogger logger)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -637,6 +637,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ApprovalWorkflowId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<Guid?>("BudgetId")
|
b.Property<Guid?>("BudgetId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
@ -657,6 +660,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<Guid?>("CreatedBy")
|
b.Property<Guid?>("CreatedBy")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int?>("CurrentApprovalLevelOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int?>("CurrentWorkflowStepIndex")
|
b.Property<int?>("CurrentWorkflowStepIndex")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@ -732,6 +738,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApprovalWorkflowId");
|
||||||
|
|
||||||
b.HasIndex("BudgetId");
|
b.HasIndex("BudgetId");
|
||||||
|
|
||||||
b.HasIndex("MaHopDong")
|
b.HasIndex("MaHopDong")
|
||||||
@ -3479,6 +3487,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Budget");
|
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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
||||||
|
|||||||
Reference in New Issue
Block a user