[CLAUDE] PurchaseEvaluation: Chunk A — BE guard target TraLai/TuChoi BẮT BUỘC decision=Reject + 3 regression test
Defense-in-depth chặn FE inconsistency (gotcha #45 — Session 21 turn 3). Bug pattern: button "← Trả lại" trong PeWorkflowPanel.tsx gửi decision=Approve khi target=TraLai do `isReject` local var thiếu nhánh TraLai → BE skip Reject branch → enter APPROVE STEP → ApproveV2Async UPSERT opinion = "đã duyệt" + advance Cấp. User UAT thấy: "Trả về nhưng hệ thống vẫn duyệt". BE guard: - Service `TransitionAsync` thêm early check sau set isAdmin/isSystem - targetPhase ∈ {TraLai, TuChoi} && decision != Reject → throw ConflictException - Boundary protection cho mọi caller tương lai (API client / mobile / cron) Tests (Infra suite +3): - TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState - TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState - TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai (happy path) + NoOpNotificationService stub reusable cho future PE service tests Verify: - dotnet test SolutionErp.slnx → 84 PASS (58 Domain + 26 Infra = +3 from 81 baseline) - Build pass (0 err, 2 warn CS8602 pre-existing DocxRenderer) Pending Chunk B: FE fix PeWorkflowPanel.tsx isReject + dialog isSendBack mirror 2 app (fe-admin + fe-user) — sync với BE guard rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -47,6 +47,26 @@ public class PurchaseEvaluationWorkflowService(
|
||||
var isAdmin = actorRoles.Contains(AppRoles.Admin);
|
||||
var isSystem = actorUserId is null && decision == ApprovalDecision.AutoApprove;
|
||||
|
||||
// ===== GUARD: targetPhase TraLai/TuChoi BẮT BUỘC decision=Reject =====
|
||||
// Defense-in-depth chặn FE inconsistency (gotcha #45 — Session 21 turn 3):
|
||||
// Bug: button "← Trả lại" trong PeWorkflowPanel.tsx gửi decision=Approve
|
||||
// khi target=TraLai do `isReject` local var thiếu nhánh TraLai. BE nhận
|
||||
// payload sẽ skip Reject branch → enter APPROVE STEP → ApproveV2Async
|
||||
// UPSERT opinion = "đã duyệt" + advance Cấp. User UAT thấy: "Trả về
|
||||
// nhưng hệ thống vẫn duyệt".
|
||||
// FE fix song song trong fe-admin + fe-user (rule §3.9 mirror 2 app).
|
||||
// Guard này KHÔNG xoá khi FE fix — boundary protection cho mọi caller
|
||||
// tương lai (API client / mobile app / cron retry).
|
||||
if ((targetPhase == PurchaseEvaluationPhase.TraLai
|
||||
|| targetPhase == PurchaseEvaluationPhase.TuChoi)
|
||||
&& decision != ApprovalDecision.Reject)
|
||||
{
|
||||
throw new ConflictException(
|
||||
$"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " +
|
||||
"Báo lỗi caller — payload mismatch giữa target phase và decision " +
|
||||
"(xem gotcha #45 + docs/workflow-contract.md).");
|
||||
}
|
||||
|
||||
// ===== REJECT BRANCH =====
|
||||
if (decision == ApprovalDecision.Reject)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user