[CLAUDE] Domain+App+Infra: Plan B Chunk C — Mig 33 ContractLevelOpinions cookie-cutter mirror PE Mig 26

- Domain/Contracts/ContractLevelOpinion.cs (NEW entity mirror PE — AuditableEntity, 4 field core + 2 nav)
- Domain/Contracts/Contract.cs (+LevelOpinions nav collection)
- Migrations/20260522052240_AddContractLevelOpinions.cs (3-file rule: .cs + .Designer.cs + Snapshot)
- Configurations/ContractLevelOpinionConfiguration.cs (NEW separate file, mirror PE pattern)
- IApplicationDbContext.cs + ApplicationDbContext.cs (+DbSet<ContractLevelOpinion>)

UNIQUE composite (ContractId, ApprovalWorkflowLevelId) — 1 row per HĐ × Level.
FK Cascade Contract + Restrict ApprovalWorkflowLevel.
SignedByUserId KHÔNG nav (denorm SignedByFullName tránh cascade khi xoá user).

Mirror PE Mig 26 pattern (S19 2026-05-09) EXACT — UPSERT row khi Approver duyệt qua
Service ApproveV2Async (Plan B Chunk B em main 138469d đã có TODO marker).
Em main sẽ add UPSERT block sau Chunk C done (Chunk D).

Verify:
- dotnet build PASS 0 err (2 pre-existing warn DocxRenderer unrelated)
- dotnet ef database update PASS (Mig 33 applied SolutionErp_Dev + _Design)
- dotnet test 111/111 PASS (58 Domain + 53 Infra — no regression)

Plan B chain (6 chunks):
- A1 58898e8  ContractApprovalWorkflowV2 entity scaffold
- A2 a85e437  Contract.ApprovalWorkflowId + ContractConfiguration FK
- B 138469d  ContractWorkflowService ApproveV2Async skeleton + TODO LevelOpinion UPSERT
- C (this)  ContractLevelOpinions entity + Mig 33 + config + DbSet
- 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:
pqhuy1987
2026-05-22 12:24:38 +07:00
parent 138469db4e
commit 26c98d3c11
8 changed files with 4265 additions and 0 deletions

View File

@ -37,6 +37,8 @@ public class ApplicationDbContext
public DbSet<ContractCodeSequence> ContractCodeSequences => Set<ContractCodeSequence>();
public DbSet<ContractChangelog> ContractChangelogs => Set<ContractChangelog>();
public DbSet<ContractDepartmentApproval> ContractDepartmentApprovals => Set<ContractDepartmentApproval>();
// Plan B Chunk C (S29 — Mig 33) — Ý kiến cấp duyệt V2 cho HĐ. Mirror PE Mig 26.
public DbSet<ContractLevelOpinion> ContractLevelOpinions => Set<ContractLevelOpinion>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments => Set<WorkflowTypeAssignment>();
public DbSet<WorkflowDefinition> WorkflowDefinitions => Set<WorkflowDefinition>();

View File

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Contracts;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// Plan B Chunk C (S29 — Mig 33, 2026-05-22) — cookie-cutter mirror
// PurchaseEvaluationLevelOpinionConfiguration (Mig 26 S19).
//
// UPSERT auto sync từ ContractWorkflowService.ApproveV2Async (Plan B Chunk D em
// main wire). UNIQUE (ContractId, ApprovalWorkflowLevelId) đảm bảo 1 row/level/HĐ.
// FK Cascade Contract (xoá HĐ → 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 ContractLevelOpinionConfiguration : IEntityTypeConfiguration<ContractLevelOpinion>
{
public void Configure(EntityTypeBuilder<ContractLevelOpinion> e)
{
e.ToTable("ContractLevelOpinions");
e.Property(x => x.Comment).HasMaxLength(2000);
e.Property(x => x.SignedByFullName).HasMaxLength(200).IsRequired();
e.HasOne(x => x.Contract)
.WithMany(c => c.LevelOpinions)
.HasForeignKey(x => x.ContractId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Level)
.WithMany()
.HasForeignKey(x => x.ApprovalWorkflowLevelId)
.OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.ContractId, 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 AddContractLevelOpinions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ContractLevelOpinions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ContractId = 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_ContractLevelOpinions", x => x.Id);
table.ForeignKey(
name: "FK_ContractLevelOpinions_ApprovalWorkflowLevels_ApprovalWorkflowLevelId",
column: x => x.ApprovalWorkflowLevelId,
principalTable: "ApprovalWorkflowLevels",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_ContractLevelOpinions_Contracts_ContractId",
column: x => x.ContractId,
principalTable: "Contracts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ContractLevelOpinions_ApprovalWorkflowLevelId",
table: "ContractLevelOpinions",
column: "ApprovalWorkflowLevelId");
migrationBuilder.CreateIndex(
name: "IX_ContractLevelOpinions_ContractId_ApprovalWorkflowLevelId",
table: "ContractLevelOpinions",
columns: new[] { "ContractId", "ApprovalWorkflowLevelId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ContractLevelOpinions");
}
}
}

View File

@ -1044,6 +1044,64 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("ContractDepartmentApprovals", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", 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<Guid>("ContractId")
.HasColumnType("uniqueidentifier");
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<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("ContractId", "ApprovalWorkflowLevelId")
.IsUnique();
b.ToTable("ContractLevelOpinions", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b =>
{
b.Property<Guid>("Id")
@ -3550,6 +3608,25 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Contract");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b =>
{
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level")
.WithMany()
.HasForeignKey("ApprovalWorkflowLevelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
.WithMany("LevelOpinions")
.HasForeignKey("ContractId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Contract");
b.Navigation("Level");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b =>
{
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
@ -3882,6 +3959,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("GiaoKhoanDetails");
b.Navigation("LevelOpinions");
b.Navigation("MuaBanDetails");
b.Navigation("NguyenTacDvDetails");