[CLAUDE] PurchaseEvaluation: Mig 26 PeLevelOpinions V2 dynamic — Chunk A Domain + EF

Schema mới cho Section 5 "Ý kiến cấp duyệt" V2 dynamic theo
ApprovalWorkflowsV2 (Mig 22-25). Thay thế Mig 15 cố định 4 box (V1).

Entity `PurchaseEvaluationLevelOpinion : AuditableEntity`:
- (PEId, ApprovalWorkflowLevelId) UNIQUE composite
- Comment nvarchar(2000) — text ý kiến hoặc "(duyệt — không ý kiến)" placeholder (Q4 bonus)
- SignedAt datetime2 (luôn có khi UPSERT từ ApproveV2Async)
- SignedByUserId Guid (NV chính chủ HOẶC Admin override)
- SignedByFullName nvarchar(200) — denorm tránh user bị xóa/đổi tên

EF: FK Cascade Pe + Restrict Level. SignedByUserId KHÔNG nav (denorm only).
Migration 26 `AddPeLevelOpinionsForV2`: 1 CREATE TABLE + 2 FK + 2 index.
3-file rule commit đủ (.cs + Designer + Snapshot).

Apply LocalDB SolutionErp_Dev OK (Mig 25 + 26 catchup).

Verify: dotnet build pass + dotnet test 81 pass (no regression).

Chunk B kế tiếp: Service V2 hook UPSERT auto trong ApproveV2Async.
This commit is contained in:
pqhuy1987
2026-05-09 10:56:16 +07:00
parent 873e7a1b7b
commit 77a30584fc
8 changed files with 4120 additions and 0 deletions

View File

@ -63,6 +63,8 @@ public class ApplicationDbContext
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();
// Mig 26 (Session 19) — Ý kiến cấp duyệt V2 dynamic
public DbSet<PurchaseEvaluationLevelOpinion> PurchaseEvaluationLevelOpinions => Set<PurchaseEvaluationLevelOpinion>();
// Quy trình duyệt mới (Mig 22 — Session 17): schema riêng UAT.
public DbSet<ApprovalWorkflow> ApprovalWorkflows => Set<ApprovalWorkflow>();

View File

@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 26 — UPSERT auto sync từ ApproveV2Async. UNIQUE (PEId, LevelId)
// đảm bảo 1 row/level/phiếu. FK Cascade Pe (xoá phiếu → xoá opinions),
// FK Restrict Level (admin xoá Level chặn nếu opinion tồn tại — bảo vệ data).
// SignedByUserId KHÔNG nav (tránh cascade khi xoá user; denorm SignedByFullName).
public class PurchaseEvaluationLevelOpinionConfiguration : IEntityTypeConfiguration<PurchaseEvaluationLevelOpinion>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationLevelOpinion> e)
{
e.ToTable("PurchaseEvaluationLevelOpinions");
e.Property(x => x.Comment).HasMaxLength(2000);
e.Property(x => x.SignedByFullName).HasMaxLength(200).IsRequired();
e.HasOne(x => x.PurchaseEvaluation)
.WithMany(p => p.LevelOpinions)
.HasForeignKey(x => x.PurchaseEvaluationId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Level)
.WithMany()
.HasForeignKey(x => x.ApprovalWorkflowLevelId)
.OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.PurchaseEvaluationId, x.ApprovalWorkflowLevelId }).IsUnique();
e.HasIndex(x => x.ApprovalWorkflowLevelId);
}
}

View File

@ -0,0 +1,69 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPeLevelOpinionsForV2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PurchaseEvaluationLevelOpinions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ApprovalWorkflowLevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Comment = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
SignedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
SignedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SignedByFullName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, 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),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationLevelOpinions", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationLevelOpinions_ApprovalWorkflowLevels_ApprovalWorkflowLevelId",
column: x => x.ApprovalWorkflowLevelId,
principalTable: "ApprovalWorkflowLevels",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PurchaseEvaluationLevelOpinions_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationLevelOpinions_ApprovalWorkflowLevelId",
table: "PurchaseEvaluationLevelOpinions",
column: "ApprovalWorkflowLevelId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationLevelOpinions_PurchaseEvaluationId_ApprovalWorkflowLevelId",
table: "PurchaseEvaluationLevelOpinions",
columns: new[] { "PurchaseEvaluationId", "ApprovalWorkflowLevelId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PurchaseEvaluationLevelOpinions");
}
}
}

View File

@ -2990,6 +2990,64 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("PurchaseEvaluationDetails", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ApprovalWorkflowLevelId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Comment")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("SignedAt")
.HasColumnType("datetime2");
b.Property<string>("SignedByFullName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid>("SignedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ApprovalWorkflowLevelId");
b.HasIndex("PurchaseEvaluationId", "ApprovalWorkflowLevelId")
.IsUnique();
b.ToTable("PurchaseEvaluationLevelOpinions", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
{
b.Property<Guid>("Id")
@ -3647,6 +3705,25 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b =>
{
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level")
.WithMany()
.HasForeignKey("ApprovalWorkflowLevelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("LevelOpinions")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Level");
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail")
@ -3787,6 +3864,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Details");
b.Navigation("LevelOpinions");
b.Navigation("Quotes");
b.Navigation("Suppliers");