[CLAUDE] PurchaseEvaluation: drafter tự gỡ cờ GẤP + ngân sách kỳ này tự nhảy từ ban-hành (anh Kiệt FDC UAT R2)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m46s

D2 — Người KHAI phiếu tự GỠ cờ GẤP của mình (nới luật S77 "GỠ=chỉ TP"). BE handler
restructure SET/UNSET: GẮN giữ role-only (PRO/CCM/Admin); GỠ +drafter (DrafterUserId==actor)
→ drafter/Admin clear CẢ 2 cờ, TP-vai clear cờ vai mình. FE canToggle +isDrafter.
D3 — "Ngân sách kỳ này" (row3) tự nhảy từ "Ngân sách Ban hành lần đầu"
(PeWorkItemBudget.ProInitialAmount) khi chưa nhập tay. Submit-guard BE + FE missingForApproval
dùng effective = budgetPeriodAmount ?? proInitialAmount (mirror 2 tầng, chống bypass gotcha #44).

BE 2 file (Urgent handler + Workflow submit-guard). FE PeDetailTabs ×2 app SHA-identical.
Test +5 PeUrgentToggleAuthzTests (drafter-self-unset authz matrix) → 371 PASS. BE slnx + 2 FE build PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-23 14:00:33 +07:00
parent 24f0083d78
commit a5e936fc0a
5 changed files with 219 additions and 42 deletions

View File

@ -42,34 +42,34 @@ public class SetPurchaseEvaluationUrgentCommandHandler(
var hasPro = roles.Contains(AppRoles.Procurement);
var hasCcm = roles.Contains(AppRoles.CostControl);
// [S77 Tra Sol/anh Kiệt — chốt 14:25] BẤT ĐỐI XỨNG theo IsUrgent:
// • GẮN (true): "ai làm người đó gắn" → NV chức năng PRO/CCM tự đánh dấu (chỉ cần role chức năng).
// • GỠ (false): "gỡ thì chỉ cho TP gỡ" → CHỈ Trưởng phòng (DeptManager) của phòng đó hoặc Admin
// (tránh NV khác lỡ tay gỡ cờ của người đã gắn).
var isPro = request.IsUrgent ? hasPro : (hasPro && isDeptManager);
var isCcm = request.IsUrgent ? hasCcm : (hasCcm && isDeptManager);
if (!isAdmin && !isPro && !isCcm)
throw new ForbiddenException(request.IsUrgent
? "Chỉ PRO (Cung ứng) / CCM (Kiểm soát chi phí) / Admin được đánh dấu phiếu gấp."
: "Chỉ Trưởng phòng Cung ứng / Kiểm soát chi phí hoặc Admin được GỠ cờ gấp.");
// [S77 Tra Sol/anh Kiệt] GẮN bất đối xứng + [S85 anh Kiệt] NGƯỜI KHAI tự gỡ cờ của mình:
// • GẮN (true): NV chức năng PRO/CCM (hoặc Admin) tự đánh dấu cờ của vai mình.
// • GỠ (false): Admin / Trưởng phòng (role chức năng + DeptManager) / chính người KHAI phiếu
// (DrafterUserId). Drafter/Admin gỡ CẢ 2 cờ (un-urgent phiếu); TP-vai chỉ gỡ cờ của vai mình.
var isDrafter = currentUser.UserId is Guid actorUid && actorUid == entity.DrafterUserId;
// Snapshot để phát hiện chuyển false→true (MỚI bật gấp) → notify CEO 1 lần.
var wasUrgent = entity.IsUrgentByPro || entity.IsUrgentByCcm;
// Role quyết định cờ nào: PRO → ĐỎ (ByPro), CCM → XANH (ByCcm), Admin → CẢ 2.
if (isAdmin)
if (request.IsUrgent)
{
entity.IsUrgentByPro = request.IsUrgent;
entity.IsUrgentByCcm = request.IsUrgent;
// GẮN — chỉ role chức năng / Admin. Role quyết cờ: PRO → ĐỎ, CCM → XANH, Admin → CẢ 2.
if (!isAdmin && !hasPro && !hasCcm)
throw new ForbiddenException("Chỉ PRO (Cung ứng) / CCM (Kiểm soát chi phí) / Admin được đánh dấu phiếu gấp.");
if (isAdmin) { entity.IsUrgentByPro = true; entity.IsUrgentByCcm = true; }
else if (hasPro) entity.IsUrgentByPro = true;
else entity.IsUrgentByCcm = true; // hasCcm
}
else if (isPro)
else
{
entity.IsUrgentByPro = request.IsUrgent;
}
else // isCcm
{
entity.IsUrgentByCcm = request.IsUrgent;
// GỠ — Admin / TP-vai / người khai phiếu. Drafter+Admin gỡ cả 2; TP-vai gỡ cờ vai mình.
var isProTp = hasPro && isDeptManager;
var isCcmTp = hasCcm && isDeptManager;
if (!isAdmin && !isProTp && !isCcmTp && !isDrafter)
throw new ForbiddenException("Chỉ Admin / Trưởng phòng Cung ứngKiểm soát chi phí / người khai phiếu được GỠ cờ gấp.");
if (isAdmin || isDrafter) { entity.IsUrgentByPro = false; entity.IsUrgentByCcm = false; }
else if (isProTp) entity.IsUrgentByPro = false;
else entity.IsUrgentByCcm = false; // isCcmTp
}
await db.SaveChangesAsync(ct);

View File

@ -198,7 +198,19 @@ public class PurchaseEvaluationWorkflowService(
// [S61 Mig 50] Schema ngân sách mới — điều kiện (3) = "Ngân sách - kỳ này"
// (BudgetPeriodAmount, drafter nhập). FE PeDetailTabs missingForApproval
// mirror CÙNG predicate (đổi đồng bộ 2 tầng).
if (evaluation.BudgetPeriodAmount is null || evaluation.BudgetPeriodAmount <= 0)
// [S85 anh Kiệt — D3] "Ngân sách kỳ này" tự nhảy từ "Ngân sách Ban hành lần đầu"
// (PeWorkItemBudget.ProInitialAmount per Dự-án×Hạng-mục). Effective = BudgetPeriodAmount
// ?? ProInitialAmount → phiếu chưa nhập tay row3 nhưng đã có ban-hành PRO vẫn gửi-duyệt được
// (FE missingForApproval mirror cùng fallback — đổi đồng bộ 2 tầng).
decimal? effectiveBudget = evaluation.BudgetPeriodAmount;
if ((effectiveBudget is null || effectiveBudget <= 0) && evaluation.WorkItemId is Guid wiId)
{
effectiveBudget = await db.PeWorkItemBudgets.AsNoTracking()
.Where(b => b.ProjectId == evaluation.ProjectId && b.WorkItemId == wiId)
.Select(b => b.ProInitialAmount)
.FirstOrDefaultAsync(ct);
}
if (effectiveBudget is null || effectiveBudget <= 0)
{
missing.Add("chưa nhập Ngân sách kỳ này");
}