[CLAUDE] PurchaseEvaluation: go han hanh dong "Tu choi" - chi con Duyet hoac Tra lai (UAT anh Kiet S60 14:14)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m30s

- Domain policy: xoa MOI transition -> TuChoi o ca 4 policy (NccOnly + NccWithPlan + ForV2Schema + FromDefinition) -> NextPhases het tra TuChoi, nut FE tu bien mat
- Service guard S60: chan targetPhase=TuChoi moi caller ke ca Admin (dung truoc moi branch — spec bo han, khong escape hatch); message huong dan dung Tra lai / Xoa nhap
- FE x2 app: filter phong thu next.filter(p != TuChoi) PeWorkflowPanel (SHA256 identical); dialog/isCancel giu dead-safe de flip lai de
- Enum TuChoi + phieu TuChoi cu + tab filter "Tu choi" GIU display (data cu render binh thuong)
- SlaExpiryJob chi dung Contract — PE khong auto-TuChoi, khong anh huong
- Tests spec-change cung commit: Domain flip BothPolicies_TuChoi_RemovedFromAllTransitions_S60 + NEW V2SchemaPolicy fact; Infra NEW TargetTuChoi_WithRejectDecision_Throws_TuChoiRemoved_S60 (guard #45 test cu giu nguyen PASS — van dung truoc)
- Test 254 -> 256 PASS (59 Domain + 197 Infra)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-12 14:30:38 +07:00
parent 37122f0f64
commit 6db195dd42
6 changed files with 98 additions and 37 deletions

View File

@ -58,22 +58,18 @@ public static class PurchaseEvaluationPolicies
{
// Drafter trình từ Nháp HOẶC gửi lại từ Trả lại — cùng entry point.
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai Phase riêng) / Từ chối (TuChoi terminal).
// Phase trung gian: 2 hành động — Duyệt forward / Trả lại (TraLai Phase riêng).
// UAT S60: TuChoi gỡ khỏi quy trình ("Duyệt hoặc Trả về thôi").
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
@ -94,30 +90,23 @@ public static class PurchaseEvaluationPolicies
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
{
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai) / Từ chối (TuChoi).
// Phase trung gian: 2 hành động — Duyệt forward / Trả lại (TraLai). UAT S60: TuChoi gỡ.
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TraLai)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
@ -160,24 +149,23 @@ public static class PurchaseEvaluationPolicyRegistry
// Workflow chạy theo state machine 5 trạng thái + iterate Steps/Levels —
// Phase enum chỉ dùng (DangSoanThao/TraLai/ChoDuyet/DaDuyet/TuChoi). Service
// tự handle advance level/step bên trong ChoDuyet, FE chỉ cần biết:
// DangSoanThao/TraLai → ChoDuyet (trình) | TuChoi (huỷ)
// ChoDuyet → ChoDuyet (advance) | TraLai (trả lại) | TuChoi (từ chối)
// DangSoanThao/TraLai → ChoDuyet (trình)
// ChoDuyet → ChoDuyet (advance) | TraLai (trả lại)
// UAT S60 (anh Kiệt): TuChoi GỠ khỏi quy trình ("Duyệt hoặc Trả về thôi") —
// enum + display phiếu TuChoi cũ GIỮ, chỉ hết đường transition tới.
public static PurchaseEvaluationPolicy ForV2Schema()
{
var transitions = new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
{
// Drafter trình từ Nháp HOẶC gửi lại từ Trả lại — cùng entry point
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoDuyet)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoDuyet)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
// ChoDuyet — Service guard match approver ApproverUserId, Policy chỉ
// expose 3 nút cho FE (Duyệt forward / Trả lại / Từ chối). Roles "*"
// để guard không block; Service tự enforce ApproverUserId match.
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.ChoDuyet)] = ["*"],
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TraLai)] = ["*"],
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TuChoi)] = ["*"],
};
var sla = new Dictionary<PurchaseEvaluationPhase, TimeSpan?>
{
@ -245,29 +233,21 @@ public static class PurchaseEvaluationPolicyRegistry
userTransitions.TryAdd((PurchaseEvaluationPhase.TraLai, s.Phase), userIds);
}
// 3 hành động phase trung gian — Duyệt forward + Trả lại (TraLai) + Từ chối (TuChoi)
// 2 hành động phase trung gian — Duyệt forward + Trả lại (TraLai).
// UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả
// về thôi" → TuChoi GỠ khỏi mọi transition map (enum + phase
// display + data cũ GIỮ; service có guard mirror chặn API direct).
if (prev.Value != PurchaseEvaluationPhase.DangSoanThao && s.Phase != PurchaseEvaluationPhase.DangSoanThao)
{
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), roles);
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), roles);
if (userIds.Length > 0)
{
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), userIds);
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), userIds);
}
}
}
prev = s.Phase;
}
// First step (DangSoanThao) — Drafter có thể TuChoi (huỷ phiếu).
// Tương tự cho TraLai — Drafter có thể TuChoi luôn từ Trả lại.
if (steps.Count > 0)
{
transitions.TryAdd((steps[0].Phase, PurchaseEvaluationPhase.TuChoi),
[AppRoles.Drafter, AppRoles.DeptManager]);
transitions.TryAdd((PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi),
[AppRoles.Drafter, AppRoles.DeptManager]);
}
// Terminal states always available + TraLai phase
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))

View File

@ -71,6 +71,20 @@ public class PurchaseEvaluationWorkflowService(
"(xem gotcha #45 + docs/workflow-contract.md).");
}
// ===== UAT S60 (anh Kiệt 14:14) — GỠ hành động "Từ chối" =====
// "Bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi." Mọi policy đã bỏ
// TuChoi khỏi NextPhases (FE hết nút); guard này chặn caller direct
// (API forge / client cũ cache) — đứng TRƯỚC mọi branch nên chặn CẢ
// Admin manual override (spec = bỏ hẳn hành động, không escape hatch).
// Phase TuChoi + phiếu TuChoi cũ GIỮ display/filter. Flip lại nếu cần:
// xóa guard này + restore transitions trong PurchaseEvaluationPolicy.
if (targetPhase == PurchaseEvaluationPhase.TuChoi)
{
throw new ConflictException(
"Hành động \"Từ chối\" đã được gỡ khỏi quy trình duyệt — chỉ còn Duyệt hoặc Trả lại. " +
"Phiếu cần dừng: dùng Trả lại để người soạn sửa, hoặc Xóa phiếu khi còn Bản nháp.");
}
// ===== REJECT BRANCH (extended Mig 28 — F1 multi-mode Trả lại) =====
if (decision == ApprovalDecision.Reject)
{