[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 main138469dđã 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): - A158898e8✅ ContractApprovalWorkflowV2 entity scaffold - A2a85e437✅ Contract.ApprovalWorkflowId + ContractConfiguration FK - B138469d✅ 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:
@ -35,6 +35,9 @@ public interface IApplicationDbContext
|
|||||||
DbSet<ContractCodeSequence> ContractCodeSequences { get; }
|
DbSet<ContractCodeSequence> ContractCodeSequences { get; }
|
||||||
DbSet<ContractChangelog> ContractChangelogs { get; }
|
DbSet<ContractChangelog> ContractChangelogs { get; }
|
||||||
DbSet<ContractDepartmentApproval> ContractDepartmentApprovals { get; }
|
DbSet<ContractDepartmentApproval> ContractDepartmentApprovals { get; }
|
||||||
|
// Plan B Chunk C (S29 — Mig 33) — Ý kiến cấp duyệt V2 dynamic cho HĐ.
|
||||||
|
// Cookie-cutter mirror PE Mig 26 PurchaseEvaluationLevelOpinions.
|
||||||
|
DbSet<ContractLevelOpinion> ContractLevelOpinions { get; }
|
||||||
DbSet<Notification> Notifications { get; }
|
DbSet<Notification> Notifications { get; }
|
||||||
DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments { get; }
|
DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments { get; }
|
||||||
DbSet<WorkflowDefinition> WorkflowDefinitions { get; }
|
DbSet<WorkflowDefinition> WorkflowDefinitions { get; }
|
||||||
|
|||||||
@ -48,6 +48,12 @@ public class Contract : AuditableEntity
|
|||||||
public List<ContractApproval> Approvals { get; set; } = new();
|
public List<ContractApproval> Approvals { get; set; } = new();
|
||||||
public List<ContractComment> Comments { get; set; } = new();
|
public List<ContractComment> Comments { get; set; } = new();
|
||||||
public List<ContractAttachment> Attachments { get; set; } = new();
|
public List<ContractAttachment> Attachments { get; set; } = new();
|
||||||
|
|
||||||
|
// Plan B Chunk C (S29 — Mig 33, 2026-05-22) — Ý kiến cấp duyệt V2 dynamic
|
||||||
|
// cookie-cutter mirror PE Mig 26. UPSERT auto từ ApproveV2Async (Plan B
|
||||||
|
// Chunk D em main wire). Section 5 FE render dynamic theo flow.steps[].levels[].
|
||||||
|
// HĐ V1 (WorkflowDefinitionId) KHÔNG dùng.
|
||||||
|
public List<ContractLevelOpinion> LevelOpinions { get; set; } = new();
|
||||||
public List<ContractChangelog> Changelogs { get; set; } = new();
|
public List<ContractChangelog> Changelogs { get; set; } = new();
|
||||||
public List<ContractDepartmentApproval> DepartmentApprovals { get; set; } = new();
|
public List<ContractDepartmentApproval> DepartmentApprovals { get; set; } = new();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
|
using SolutionErp.Domain.Common;
|
||||||
|
|
||||||
|
namespace SolutionErp.Domain.Contracts;
|
||||||
|
|
||||||
|
// "Ý kiến cấp duyệt" V2 cho HĐ — sign-off DYNAMIC theo workflow ApprovalWorkflowV2
|
||||||
|
// (Mig 22-25 + Plan B Mig 32). Cookie-cutter mirror PE Mig 26
|
||||||
|
// `PurchaseEvaluationLevelOpinion` (S19 2026-05-09).
|
||||||
|
//
|
||||||
|
// Mỗi row = 1 (Contract × ApprovalWorkflowLevel). Service `ApproveV2Async` sau khi
|
||||||
|
// approve thành công Cấp hiện tại sẽ UPSERT row này (Plan B Chunk D em main wire):
|
||||||
|
// Comment = approval.Comment ?? "(duyệt — không ý kiến)"
|
||||||
|
// SignedAt = clock.UtcNow
|
||||||
|
// SignedByUserId = actor.Id (NV chính chủ HOẶC Admin override)
|
||||||
|
// SignedByFullName = actor.FullName (denorm — tránh user bị xóa/đổi tên)
|
||||||
|
//
|
||||||
|
// Reject (Trả lại / Từ chối) KHÔNG sync (vì không phải sign-off của level đó).
|
||||||
|
// Khi user resubmit từ TraLai → workflow chạy lại từ Cấp 1, opinion cũ bị
|
||||||
|
// OVERWRITE bằng UPSERT mới (latest-write-wins).
|
||||||
|
//
|
||||||
|
// Section 5 FE detect V2 qua `contract.approvalWorkflowId != null` → render dynamic
|
||||||
|
// theo flow.steps[].levels[]. HĐ V1 legacy (WorkflowDefinitionId set) → fallback
|
||||||
|
// không có ý kiến dynamic (giống PE V1 behavior).
|
||||||
|
public class ContractLevelOpinion : AuditableEntity
|
||||||
|
{
|
||||||
|
public Guid ContractId { get; set; }
|
||||||
|
public Guid ApprovalWorkflowLevelId { get; set; }
|
||||||
|
|
||||||
|
public string? Comment { get; set; } // ý kiến (max 2000) hoặc placeholder "(duyệt — không ý kiến)"
|
||||||
|
public DateTime SignedAt { get; set; } // luôn có khi UPSERT (Service set khi Approve)
|
||||||
|
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể là Admin thay NV)
|
||||||
|
public string SignedByFullName { get; set; } = string.Empty; // snapshot tên — denorm
|
||||||
|
|
||||||
|
public Contract? Contract { get; set; }
|
||||||
|
public ApprovalWorkflowLevel? Level { get; set; }
|
||||||
|
}
|
||||||
@ -37,6 +37,8 @@ public class ApplicationDbContext
|
|||||||
public DbSet<ContractCodeSequence> ContractCodeSequences => Set<ContractCodeSequence>();
|
public DbSet<ContractCodeSequence> ContractCodeSequences => Set<ContractCodeSequence>();
|
||||||
public DbSet<ContractChangelog> ContractChangelogs => Set<ContractChangelog>();
|
public DbSet<ContractChangelog> ContractChangelogs => Set<ContractChangelog>();
|
||||||
public DbSet<ContractDepartmentApproval> ContractDepartmentApprovals => Set<ContractDepartmentApproval>();
|
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<Notification> Notifications => Set<Notification>();
|
||||||
public DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments => Set<WorkflowTypeAssignment>();
|
public DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments => Set<WorkflowTypeAssignment>();
|
||||||
public DbSet<WorkflowDefinition> WorkflowDefinitions => Set<WorkflowDefinition>();
|
public DbSet<WorkflowDefinition> WorkflowDefinitions => Set<WorkflowDefinition>();
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1044,6 +1044,64 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("ContractDepartmentApprovals", (string)null);
|
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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -3550,6 +3608,25 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Contract");
|
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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
||||||
@ -3882,6 +3959,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.Navigation("GiaoKhoanDetails");
|
b.Navigation("GiaoKhoanDetails");
|
||||||
|
|
||||||
|
b.Navigation("LevelOpinions");
|
||||||
|
|
||||||
b.Navigation("MuaBanDetails");
|
b.Navigation("MuaBanDetails");
|
||||||
|
|
||||||
b.Navigation("NguyenTacDvDetails");
|
b.Navigation("NguyenTacDvDetails");
|
||||||
|
|||||||
Reference in New Issue
Block a user