From 7926c2129c968b2d359e0f7bd1cb79249e2f7c60 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sat, 13 Jun 2026 11:13:10 +0700 Subject: [PATCH] [CLAUDE] PurchaseEvaluation: cho luu khi vuot ngan sach - chi canh bao mem (bo chan so am "Gia tri thuc hien du kien con lai") (UAT anh Kiet S62) - Root cause: o "Gia tri thuc hien du kien con lai" (row 8 bang Tong hop ngan sach) khi gia tri NCC vuot ngan sach -> so du con lai ra AM; BE validator ExpectedRemainingAmount>=0 + FE VndInlineEdit khong bat allowNegative -> chan cung "am ko luu duoc" (testing bao qua anh Kiet) - BE: AdjustPurchaseEvaluationBudgetCommandValidator GO rule ExpectedRemainingAmount.GreaterThanOrEqualTo(0) -> cho luu so am (mirror tien le LeaveBalance AllowsNegativeRemaining). GIU BudgetPeriodAmount>0 + submit-guard "da nhap NS ky nay" khong doi - FE x2 app SHA256 identical: (a) allowNegative cho VndInlineEdit row 8; (b) banner amber "Vuot ngan sach - van luu & gui duyet duoc" trong PeBudgetSummaryTable khi cmpPeriod<0 || cmpFull<0. Tang to mau do cu GIU NGUYEN - Spec change: flip test AdjustBudget_Validator_ExpectedRemainingNegative_FailsValidation -> _PassesValidation (am gio hop le); test BudgetPeriodZero_FailsValidation GIU (budget>0 van enforced) - Build FE x2 PASS + test 263 PASS (45 Domain + 218 Infra, 0 fail/skip). Reviewer PASS 0 issue (row8 am an toan arithmetic additive-only, submit guard nguyen, mirror byte-identical, no scope creep) Co-Authored-By: Claude Opus 4.8 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 14 ++++++++++++++ fe-user/src/components/pe/PeDetailTabs.tsx | 14 ++++++++++++++ .../PurchaseEvaluationFeatures.cs | 6 +++++- .../Application/PeWorkItemBudgetTests.cs | 10 +++++----- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index ca22477..3aaf94b 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -1052,6 +1052,19 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: Tổng hợp ngân sách trình ký + {/* [S62] Cảnh báo MỀM "vượt ngân sách" (anh Kiệt FDC) — KHÔNG chặn lưu, chỉ báo. + Hiện khi đề xuất kỳ này > NS kỳ này (cmpPeriod<0) hoặc tổng thực hiện > NS full + (cmpFull<0). Số dư còn lại âm vẫn lưu + gửi duyệt được. */} + {(cmpPeriod < 0 || cmpFull < 0) && ( +
+ ⚠️ + + Vượt ngân sách — giá trị đề xuất NCC đang cao hơn ngân sách của gói thầu. + Phiếu vẫn lưu & gửi duyệt được, vui lòng kiểm tra lại số liệu trước khi trình. + +
+ )} + {/* ===== Block A — NGÂN SÁCH (gói thầu) ===== */} A. Ngân sách (gói thầu) @@ -1253,6 +1266,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: {drafterEditable ? ( adjustMut.mutate({ budgetPeriodAmount: ev.budgetPeriodAmount, expectedRemainingAmount: v })} diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index ca22477..3aaf94b 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -1052,6 +1052,19 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: Tổng hợp ngân sách trình ký + {/* [S62] Cảnh báo MỀM "vượt ngân sách" (anh Kiệt FDC) — KHÔNG chặn lưu, chỉ báo. + Hiện khi đề xuất kỳ này > NS kỳ này (cmpPeriod<0) hoặc tổng thực hiện > NS full + (cmpFull<0). Số dư còn lại âm vẫn lưu + gửi duyệt được. */} + {(cmpPeriod < 0 || cmpFull < 0) && ( +
+ ⚠️ + + Vượt ngân sách — giá trị đề xuất NCC đang cao hơn ngân sách của gói thầu. + Phiếu vẫn lưu & gửi duyệt được, vui lòng kiểm tra lại số liệu trước khi trình. + +
+ )} + {/* ===== Block A — NGÂN SÁCH (gói thầu) ===== */} A. Ngân sách (gói thầu) @@ -1253,6 +1266,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: {drafterEditable ? ( adjustMut.mutate({ budgetPeriodAmount: ev.budgetPeriodAmount, expectedRemainingAmount: v })} diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs index 59293a5..f63453d 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs @@ -319,7 +319,11 @@ public class AdjustPurchaseEvaluationBudgetCommandValidator : AbstractValidator< public AdjustPurchaseEvaluationBudgetCommandValidator() { RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0).When(x => x.BudgetPeriodAmount.HasValue); - RuleFor(x => x.ExpectedRemainingAmount).GreaterThanOrEqualTo(0).When(x => x.ExpectedRemainingAmount.HasValue); + // [S62] "Giá trị thực hiện dự kiến còn lại" (row 8) CHO PHÉP ÂM. Khi giá trị NCC + // vượt ngân sách → số dư còn lại tự ra âm; nghiệp vụ (anh Kiệt FDC 2026-06-13) + // yêu cầu VẪN lưu được, chỉ cảnh báo (FE banner "Vượt ngân sách"). Gỡ ràng buộc + // GreaterThanOrEqualTo(0) cũ — mirror tiền lệ LeaveBalance (cho phép số dư âm). + // BudgetPeriodAmount vẫn phải > 0; submit guard "đã nhập NS kỳ này" không đổi. } } diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkItemBudgetTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkItemBudgetTests.cs index 11f7901..73f0d93 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkItemBudgetTests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkItemBudgetTests.cs @@ -652,15 +652,15 @@ public class PeWorkItemBudgetTests } [Fact] - public void AdjustBudget_Validator_ExpectedRemainingNegative_FailsValidation() + public void AdjustBudget_Validator_ExpectedRemainingNegative_PassesValidation() { - // GreaterThanOrEqualTo(0) when HasValue → âm không hợp lệ (row 8 không cho âm). + // [S62] SPEC CHANGE — "Giá trị thực hiện dự kiến còn lại" (row 8) ÂM giờ HỢP LỆ. + // Vượt ngân sách → số dư còn lại ra âm; nghiệp vụ (anh Kiệt FDC) cho lưu, chỉ + // cảnh báo. Rule GreaterThanOrEqualTo(0) cũ đã gỡ. BudgetPeriodAmount=80 > 0 hợp lệ. var validator = new AdjustPurchaseEvaluationBudgetCommandValidator(); var result = validator.Validate(new AdjustPurchaseEvaluationBudgetCommand(Guid.NewGuid(), 80m, -1m)); - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => - e.PropertyName == nameof(AdjustPurchaseEvaluationBudgetCommand.ExpectedRemainingAmount)); + result.IsValid.Should().BeTrue(); } [Fact]