[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

@ -48,6 +48,12 @@ public class Contract : AuditableEntity
public List<ContractApproval> Approvals { get; set; } = new();
public List<ContractComment> Comments { 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<ContractDepartmentApproval> DepartmentApprovals { get; set; } = new();

View File

@ -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; }
}