[CLAUDE] App: Chunk B — Lock edit guards (Phase != DangSoanThao) cho 17 handler

Ràng buộc 1 (Phase 9): khi đã trình duyệt → KHÔNG sửa được Header + Detail
+ Quote nữa. Phải reject để Drafter sửa lại.

Pattern dùng: extend helper EnsureContractType + tạo helper PurchaseEvaluationDraftGuard
mới cho PE + inline guard cho Budget. Single source of truth cho mỗi module.

Handlers added Phase guard (17 total):

Contract module (15) — qua EnsureContractType helper:
- 7 Add*DetailHandler (ThauPhu/GiaoKhoan/NhaCungCap/DichVu/MuaBan/NguyenTacNcc/NguyenTacDv)
- 7 Update*DetailHandler (cùng 7 type)
- DeleteContractDetailHandler (inline guard)

PE module (5) — qua PurchaseEvaluationDraftGuard helper mới:
- AddPurchaseEvaluationDetail
- UpdatePurchaseEvaluationDetail
- DeletePurchaseEvaluationDetail
- UpsertPurchaseEvaluationQuote
- DeletePurchaseEvaluationQuote

Budget module (3) — inline guard:
- AddBudgetDetail
- UpdateBudgetDetail (refactor: load Budget thay vì FirstOrDefault sau Detail
  load → bỏ null check không cần)
- DeleteBudgetDetail (refactor: tương tự)

KHÔNG lock (intentional):
- ContractComment (cần được trong DangGopY phase 3)
- ContractAttachment Upload/Delete (Drafter scan ký ở DangInKy phase 5)
- PE OpinionUpsert (Ý kiến 4 PB là sign-off, có thể nhập sau khi trình)
- PE Attachment (báo giá NCC upload xuyên suốt workflow)

Verify:
- Build pass (2 warning DocxRenderer cũ)
- 77 unit test pass (54 Domain + 23 Infra) — domain policy không đổi

Smart reject (ràng buộc 2) + 2-stage dept approval (ràng buộc 3) làm ở
Chunk C + D. WorkflowService transition guard chưa update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-04 12:15:07 +07:00
parent 5c200978cb
commit 14f3c9f817
3 changed files with 44 additions and 10 deletions

View File

@ -424,6 +424,9 @@ public class DeleteContractDetailHandler(IApplicationDbContext db, IChangelogSer
{
var contract = await db.Contracts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == cmd.ContractId, ct)
?? throw new NotFoundException("Contract", cmd.ContractId);
// Lock edit guard (Phase 9 — Migration 16): chỉ Phase=DangSoanThao xóa được.
if (contract.Phase != ContractPhase.DangSoanThao)
throw new ConflictException($"HĐ đã trình duyệt (Phase={contract.Phase}), không thể xóa chi tiết. Phải reject để Drafter sửa lại.");
// Dispatch xóa theo Type — tránh load tất cả 7 DbSet
bool removed = false;
@ -477,6 +480,10 @@ internal static class ContractDetailsHelpers
?? throw new NotFoundException("Contract", contractId);
if (contract.Type != expectedType)
throw new ConflictException($"HĐ này thuộc loại {contract.Type}, không thể thêm chi tiết loại {expectedType}.");
// Lock edit guard (Phase 9 — Migration 16): chỉ Phase=DangSoanThao mới
// được CRUD chi tiết. Đã trình duyệt → KHÔNG sửa được thông tin nữa.
if (contract.Phase != ContractPhase.DangSoanThao)
throw new ConflictException($"HĐ đã trình duyệt (Phase={contract.Phase}), không thể chỉnh sửa chi tiết. Phải reject để Drafter sửa lại.");
return contract;
}
}