[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.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);
|
||||
|
||||
@ -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)
|
||||
|
||||
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()
|
||||
.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")
|
||||
|
||||
Reference in New Issue
Block a user