[CLAUDE] Domain+Infra: Migration 20 Contract workflow inner steps mirror PE (Chunk A)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Mirror PE N-stage Mig 18+19 pattern sang Contract module. Gộp 1 migration (CREATE TABLE + ALTER + filtered unique alter) thay vì tách 2 như PE. Schema: - entity WorkflowStepInnerStep (Domain/Contracts/) — Order, DeptId, PositionLevel, Name, SlaDays, IsRequired - WorkflowStep nav +InnerSteps List - ContractDepartmentApproval +InnerStepId Guid? FK Restrict - CREATE TABLE WorkflowStepInnerSteps (no Contract prefix vì module dùng entity gốc Workflow* từ Mig 8) - DROP UX_ContractDeptApprovals_Contract_Phase_Dept_Stage cũ - RECREATE filtered: WHERE InnerStepId IS NULL (legacy 2-stage Mig 16) + new filtered UNIQUE (ContractId, Phase, InnerStepId) WHERE InnerStepId IS NOT NULL (N-stage) - 3 INDEX: IX_InnerStepId, IX_(StepId, Order), IX_DeptId Backward compat 100%: workflow Contract no InnerSteps configured → service fallback legacy 2-stage. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index. Note: Budget defer (chưa có versioned WorkflowDefinition entity — hardcoded BudgetPolicy.Default). Cần migration AddBudgetVersionedWorkflow trước khi mirror N-stage Budget. Verify: - dotnet build SolutionErp.slnx 0 error - dotnet ef database update LocalDB applied OK - dotnet test 89 pass (54 + 35) — no regression Pending Chunk B: WorkflowAdminFeatures.cs DTO/Input/Validator/Handler extend mirror PE Chunk B pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -23,5 +23,9 @@ public class ContractDepartmentApproval : AuditableEntity
|
||||
public DateTime ApprovedAt { get; set; }
|
||||
public bool IsBypassed { get; set; } // true nếu NV bypass (User.CanBypassReview=true)
|
||||
|
||||
// N-stage inner step link (Mig 20) — null cho data legacy 2-stage Review/Confirm.
|
||||
// Có giá trị khi step cha có InnerSteps configured. Mirror PE Mig 18 pattern.
|
||||
public Guid? InnerStepId { get; set; }
|
||||
|
||||
public Contract? Contract { get; set; }
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Identity; // reuse PositionLevel
|
||||
|
||||
namespace SolutionErp.Domain.Contracts;
|
||||
|
||||
@ -32,6 +33,11 @@ public class WorkflowStep : BaseEntity
|
||||
|
||||
public WorkflowDefinition? WorkflowDefinition { get; set; }
|
||||
public List<WorkflowStepApprover> Approvers { get; set; } = new();
|
||||
|
||||
// Inner steps (Mig 20) — N-stage approval Phòng × PositionLevel sequential.
|
||||
// Mirror PE pattern (Mig 18). Empty list → service fallback logic 2-stage
|
||||
// Review/Confirm legacy (Mig 16) per dept.
|
||||
public List<WorkflowStepInnerStep> InnerSteps { get; set; } = new();
|
||||
}
|
||||
|
||||
public enum WorkflowApproverKind
|
||||
@ -48,3 +54,23 @@ public class WorkflowStepApprover : BaseEntity
|
||||
|
||||
public WorkflowStep? Step { get; set; }
|
||||
}
|
||||
|
||||
// Inner step (Mig 20 — Phase 9+) — sub-step level con cấu hình bên trong 1
|
||||
// WorkflowStep cha (= 1 phase). Mirror PurchaseEvaluationWorkflowStepInnerStep
|
||||
// pattern (Mig 18). Cho phép admin định nghĩa thứ tự duyệt N-stage theo
|
||||
// Department × PositionLevel: NV.A → PP.A → TP.A → NV.B → PP.B → TP.B → ...
|
||||
//
|
||||
// User khớp DepartmentId + PositionLevel + Order tiếp theo chưa duyệt = approver
|
||||
// hợp lệ. CanBypassReview ở User cho TP skip NV+PP cùng dept (audit IsBypassed).
|
||||
public class WorkflowStepInnerStep : BaseEntity
|
||||
{
|
||||
public Guid WorkflowStepId { get; set; }
|
||||
public int Order { get; set; }
|
||||
public Guid DepartmentId { get; set; }
|
||||
public PositionLevel PositionLevel { get; set; } // NV / PP / TP
|
||||
public string? Name { get; set; } // hiển thị FE — vd "NV.PRO duyệt"
|
||||
public int? SlaDays { get; set; }
|
||||
public bool IsRequired { get; set; } = true;
|
||||
|
||||
public WorkflowStep? Step { get; set; }
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ public class ApplicationDbContext
|
||||
public DbSet<WorkflowDefinition> WorkflowDefinitions => Set<WorkflowDefinition>();
|
||||
public DbSet<WorkflowStep> WorkflowSteps => Set<WorkflowStep>();
|
||||
public DbSet<WorkflowStepApprover> WorkflowStepApprovers => Set<WorkflowStepApprover>();
|
||||
public DbSet<WorkflowStepInnerStep> WorkflowStepInnerSteps => Set<WorkflowStepInnerStep>();
|
||||
public DbSet<ThauPhuDetail> ThauPhuDetails => Set<ThauPhuDetail>();
|
||||
public DbSet<GiaoKhoanDetail> GiaoKhoanDetails => Set<GiaoKhoanDetail>();
|
||||
public DbSet<NhaCungCapDetail> NhaCungCapDetails => Set<NhaCungCapDetail>();
|
||||
|
||||
@ -27,9 +27,19 @@ public class ContractDepartmentApprovalConfiguration
|
||||
b.Property(x => x.ApproverRoleSnapshot).HasMaxLength(100);
|
||||
b.Property(x => x.Comment).HasMaxLength(1000);
|
||||
|
||||
// Legacy 2-stage rows (Mig 16): UNIQUE chỉ áp khi InnerStepId IS NULL
|
||||
// (Mig 20 mirror PE Mig 19 filtered split).
|
||||
b.HasIndex(x => new { x.ContractId, x.PhaseAtApproval, x.DepartmentId, x.Stage })
|
||||
.IsUnique()
|
||||
.HasFilter("[InnerStepId] IS NULL")
|
||||
.HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage");
|
||||
|
||||
// N-stage rows (Mig 20): UNIQUE 1 row per (phase × inner step).
|
||||
b.HasIndex(x => new { x.ContractId, x.PhaseAtApproval, x.InnerStepId })
|
||||
.IsUnique()
|
||||
.HasFilter("[InnerStepId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_InnerStep");
|
||||
|
||||
b.HasIndex(x => x.ContractId);
|
||||
b.HasIndex(x => x.DepartmentId);
|
||||
b.HasIndex(x => x.ApproverUserId);
|
||||
@ -38,6 +48,12 @@ public class ContractDepartmentApprovalConfiguration
|
||||
.WithMany(c => c.DepartmentApprovals)
|
||||
.HasForeignKey(x => x.ContractId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// FK InnerStepId nullable — Restrict (không xóa InnerStep nếu còn approval row)
|
||||
b.HasOne<WorkflowStepInnerStep>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.InnerStepId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,3 +52,31 @@ public class WorkflowStepApproverConfiguration : IEntityTypeConfiguration<Workfl
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
// Inner step (Mig 20) — N-stage approval cấu hình động trong cùng 1 phase.
|
||||
// Mirror PurchaseEvaluationWorkflowStepInnerStepConfiguration (Mig 18).
|
||||
public class WorkflowStepInnerStepConfiguration
|
||||
: IEntityTypeConfiguration<WorkflowStepInnerStep>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WorkflowStepInnerStep> e)
|
||||
{
|
||||
e.ToTable("WorkflowStepInnerSteps");
|
||||
e.HasKey(x => x.Id);
|
||||
|
||||
e.Property(x => x.PositionLevel).HasConversion<int>();
|
||||
e.Property(x => x.Name).HasMaxLength(200);
|
||||
|
||||
e.HasOne(x => x.Step)
|
||||
.WithMany(s => s.InnerSteps)
|
||||
.HasForeignKey(x => x.WorkflowStepId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasOne<SolutionErp.Domain.Master.Department>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DepartmentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.WorkflowStepId, x.Order });
|
||||
e.HasIndex(x => x.DepartmentId);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_Dept_Stage",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "InnerStepId",
|
||||
table: "ContractDepartmentApprovals",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkflowStepInnerSteps",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
WorkflowStepId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Order = table.Column<int>(type: "int", nullable: false),
|
||||
DepartmentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
PositionLevel = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
SlaDays = table.Column<int>(type: "int", nullable: true),
|
||||
IsRequired = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkflowStepInnerSteps", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkflowStepInnerSteps_Departments_DepartmentId",
|
||||
column: x => x.DepartmentId,
|
||||
principalTable: "Departments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_WorkflowStepInnerSteps_WorkflowSteps_WorkflowStepId",
|
||||
column: x => x.WorkflowStepId,
|
||||
principalTable: "WorkflowSteps",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractDepartmentApprovals_InnerStepId",
|
||||
table: "ContractDepartmentApprovals",
|
||||
column: "InnerStepId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_Dept_Stage",
|
||||
table: "ContractDepartmentApprovals",
|
||||
columns: new[] { "ContractId", "PhaseAtApproval", "DepartmentId", "Stage" },
|
||||
unique: true,
|
||||
filter: "[InnerStepId] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_InnerStep",
|
||||
table: "ContractDepartmentApprovals",
|
||||
columns: new[] { "ContractId", "PhaseAtApproval", "InnerStepId" },
|
||||
unique: true,
|
||||
filter: "[InnerStepId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkflowStepInnerSteps_DepartmentId",
|
||||
table: "WorkflowStepInnerSteps",
|
||||
column: "DepartmentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkflowStepInnerSteps_WorkflowStepId_Order",
|
||||
table: "WorkflowStepInnerSteps",
|
||||
columns: new[] { "WorkflowStepId", "Order" });
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ContractDepartmentApprovals_WorkflowStepInnerSteps_InnerStepId",
|
||||
table: "ContractDepartmentApprovals",
|
||||
column: "InnerStepId",
|
||||
principalTable: "WorkflowStepInnerSteps",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ContractDepartmentApprovals_WorkflowStepInnerSteps_InnerStepId",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkflowStepInnerSteps");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ContractDepartmentApprovals_InnerStepId",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_Dept_Stage",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_InnerStep",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "InnerStepId",
|
||||
table: "ContractDepartmentApprovals");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ContractDeptApprovals_Contract_Phase_Dept_Stage",
|
||||
table: "ContractDepartmentApprovals",
|
||||
columns: new[] { "ContractId", "PhaseAtApproval", "DepartmentId", "Stage" },
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -824,6 +824,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<Guid>("DepartmentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("InnerStepId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsBypassed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
@ -850,9 +853,17 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("DepartmentId");
|
||||
|
||||
b.HasIndex("InnerStepId");
|
||||
|
||||
b.HasIndex("ContractId", "PhaseAtApproval", "InnerStepId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_InnerStep")
|
||||
.HasFilter("[InnerStepId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ContractId", "PhaseAtApproval", "DepartmentId", "Stage")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage");
|
||||
.HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage")
|
||||
.HasFilter("[InnerStepId] IS NULL");
|
||||
|
||||
b.ToTable("ContractDepartmentApprovals", (string)null);
|
||||
});
|
||||
@ -1477,6 +1488,55 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("WorkflowStepApprovers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepInnerStep", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("DepartmentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsRequired")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PositionLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("SlaDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("WorkflowStepId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DepartmentId");
|
||||
|
||||
b.HasIndex("WorkflowStepId", "Order");
|
||||
|
||||
b.ToTable("WorkflowStepInnerSteps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -3282,6 +3342,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SolutionErp.Domain.Contracts.WorkflowStepInnerStep", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("InnerStepId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Contract");
|
||||
});
|
||||
|
||||
@ -3384,6 +3449,23 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Step");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepInnerStep", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Master.Department", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DepartmentId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step")
|
||||
.WithMany("InnerSteps")
|
||||
.HasForeignKey("WorkflowStepId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Step");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent")
|
||||
@ -3611,6 +3693,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b =>
|
||||
{
|
||||
b.Navigation("Approvers");
|
||||
|
||||
b.Navigation("InnerSteps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
|
||||
|
||||
Reference in New Issue
Block a user