[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
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:
@ -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 ứng–Kiể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);
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user