[CLAUDE] Domain+Infra: Migration 18 PE workflow inner steps + User.PositionLevel (Chunk A)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled

N-stage workflow approval — mỗi WorkflowStep cha (= 1 phase) cấu hình
được chuỗi InnerSteps con theo Department × PositionLevel với Order
sequential. Phase 9+ feature, mở rộng từ 2-stage Mig 16.

Schema:
- enum PositionLevel { NhanVien=1, PhoPhong=2, TruongPhong=3 } (Domain/Identity)
- ALTER Users + PositionLevel int? NULL (admin/system user vẫn null)
- CREATE TABLE PurchaseEvaluationWorkflowStepInnerSteps:
  Id PK, PurchaseEvaluationWorkflowStepId FK Cascade,
  Order int, DepartmentId FK Restrict, PositionLevel int,
  Name nvarchar(200), SlaDays int?, IsRequired bit
- ALTER PurchaseEvaluationDepartmentApprovals + InnerStepId Guid? FK Restrict
  (null cho data legacy 2-stage Review/Confirm Mig 16)

Backward compat: step KHÔNG có InnerSteps → service fallback logic
2-stage Stage=Review|Confirm cũ (Chunk C). Data Mig 16 hiện có giữ
nguyên, InnerStepId=null.

Verify:
- dotnet build SolutionErp.slnx pass (0 error, 2 pre-existing warning DocxRenderer)
- dotnet ef database update LocalDB applied OK
- dotnet test SolutionErp.slnx 83 pass (54 Domain + 29 Infra) — no regression
- 3-file rule: Migration.cs + Designer.cs + Snapshot updated

Pending Chunk B: Application CQRS — extend CreatePeWorkflowDefinitionCommand
với InnerSteps DTO + UpdateUserPositionLevelCommand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 18:11:42 +07:00
parent 130903fe1b
commit 13ab533fe7
10 changed files with 3944 additions and 1 deletions

View File

@ -59,6 +59,7 @@ public class ApplicationDbContext
public DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions => Set<PurchaseEvaluationWorkflowDefinition>();
public DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps => Set<PurchaseEvaluationWorkflowStep>();
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
public DbSet<PurchaseEvaluationWorkflowStepInnerStep> PurchaseEvaluationWorkflowStepInnerSteps => Set<PurchaseEvaluationWorkflowStepInnerStep>();
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();

View File

@ -59,11 +59,19 @@ public class PurchaseEvaluationDepartmentApprovalConfiguration
b.HasIndex(x => x.PurchaseEvaluationId);
b.HasIndex(x => x.DepartmentId);
b.HasIndex(x => x.ApproverUserId);
b.HasIndex(x => x.InnerStepId); // Mig 18 — query rows by sub-step
b.HasOne(x => x.PurchaseEvaluation)
.WithMany(c => c.DepartmentApprovals)
.HasForeignKey(x => x.PurchaseEvaluationId)
.OnDelete(DeleteBehavior.Cascade);
// FK InnerStepId nullable — Restrict (không xóa InnerStep nếu còn approval row).
// Cấu hình không nav để giữ nhẹ entity (1 chiều, query qua join nếu cần).
b.HasOne<PurchaseEvaluationWorkflowStepInnerStep>()
.WithMany()
.HasForeignKey(x => x.InnerStepId)
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@ -209,6 +209,37 @@ public class PurchaseEvaluationWorkflowStepApproverConfiguration : IEntityTypeCo
}
}
// Inner step (Mig 18) — N-stage approval cấu hình động trong cùng 1 phase.
// FK Cascade từ Step cha. Index theo (StepId, Order) cho query ordered list.
// Index riêng DepartmentId để lookup khi service compute next pending sub-step.
public class PurchaseEvaluationWorkflowStepInnerStepConfiguration
: IEntityTypeConfiguration<PurchaseEvaluationWorkflowStepInnerStep>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationWorkflowStepInnerStep> e)
{
e.ToTable("PurchaseEvaluationWorkflowStepInnerSteps");
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.PurchaseEvaluationWorkflowStepId)
.OnDelete(DeleteBehavior.Cascade);
// FK Department — Restrict (không xóa dept nếu còn inner step assigned).
// Không cấu hình nav trên Department để tránh circular collection bloat.
e.HasOne<SolutionErp.Domain.Master.Department>()
.WithMany()
.HasForeignKey(x => x.DepartmentId)
.OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.PurchaseEvaluationWorkflowStepId, x.Order });
e.HasIndex(x => x.DepartmentId);
}
}
// Mirror ContractCodeSequenceConfiguration — Prefix là PK, atomic UPDATE qua
// SERIALIZABLE transaction trong PurchaseEvaluationCodeGenerator.
public class PurchaseEvaluationCodeSequenceConfiguration

View File

@ -0,0 +1,107 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPeWorkflowInnerStepsAndPositionLevel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PositionLevel",
table: "Users",
type: "int",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals",
type: "uniqueidentifier",
nullable: true);
migrationBuilder.CreateTable(
name: "PurchaseEvaluationWorkflowStepInnerSteps",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationWorkflowStepId = 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_PurchaseEvaluationWorkflowStepInnerSteps", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationWorkflowStepInnerSteps_Departments_DepartmentId",
column: x => x.DepartmentId,
principalTable: "Departments",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PurchaseEvaluationWorkflowStepInnerSteps_PurchaseEvaluationWorkflowSteps_PurchaseEvaluationWorkflowStepId",
column: x => x.PurchaseEvaluationWorkflowStepId,
principalTable: "PurchaseEvaluationWorkflowSteps",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationDepartmentApprovals_InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals",
column: "InnerStepId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowStepInnerSteps_DepartmentId",
table: "PurchaseEvaluationWorkflowStepInnerSteps",
column: "DepartmentId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowStepInnerSteps_PurchaseEvaluationWorkflowStepId_Order",
table: "PurchaseEvaluationWorkflowStepInnerSteps",
columns: new[] { "PurchaseEvaluationWorkflowStepId", "Order" });
migrationBuilder.AddForeignKey(
name: "FK_PurchaseEvaluationDepartmentApprovals_PurchaseEvaluationWorkflowStepInnerSteps_InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals",
column: "InnerStepId",
principalTable: "PurchaseEvaluationWorkflowStepInnerSteps",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PurchaseEvaluationDepartmentApprovals_PurchaseEvaluationWorkflowStepInnerSteps_InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals");
migrationBuilder.DropTable(
name: "PurchaseEvaluationWorkflowStepInnerSteps");
migrationBuilder.DropIndex(
name: "IX_PurchaseEvaluationDepartmentApprovals_InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals");
migrationBuilder.DropColumn(
name: "PositionLevel",
table: "Users");
migrationBuilder.DropColumn(
name: "InnerStepId",
table: "PurchaseEvaluationDepartmentApprovals");
}
}
}

View File

@ -1809,6 +1809,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int?>("PositionLevel")
.HasColumnType("int");
b.Property<string>("RefreshToken")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
@ -2656,6 +2659,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<Guid>("DepartmentId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("InnerStepId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsBypassed")
.HasColumnType("bit");
@ -2683,6 +2689,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasIndex("DepartmentId");
b.HasIndex("InnerStepId");
b.HasIndex("PurchaseEvaluationId");
b.HasIndex("PurchaseEvaluationId", "PhaseAtApproval", "DepartmentId", "Stage")
@ -3072,6 +3080,55 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepInnerStep", 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<Guid>("PurchaseEvaluationWorkflowStepId")
.HasColumnType("uniqueidentifier");
b.Property<int?>("SlaDays")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("DepartmentId");
b.HasIndex("PurchaseEvaluationWorkflowStepId", "Order");
b.ToTable("PurchaseEvaluationWorkflowStepInnerSteps", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.Role", null)
@ -3393,6 +3450,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepInnerStep", null)
.WithMany()
.HasForeignKey("InnerStepId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("DepartmentApprovals")
.HasForeignKey("PurchaseEvaluationId")
@ -3480,6 +3542,23 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Step");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepInnerStep", b =>
{
b.HasOne("SolutionErp.Domain.Master.Department", null)
.WithMany()
.HasForeignKey("DepartmentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step")
.WithMany("InnerSteps")
.HasForeignKey("PurchaseEvaluationWorkflowStepId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Step");
});
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
{
b.Navigation("Approvals");
@ -3567,6 +3646,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b =>
{
b.Navigation("Approvers");
b.Navigation("InnerSteps");
});
#pragma warning restore 612, 618
}