[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
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:
@ -136,7 +136,10 @@ export function PeWorkflowPanel({
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const next = evaluation.workflow.nextPhases
|
// UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi".
|
||||||
|
// BE policy đã gỡ TuChoi khỏi nextPhases; filter này = defense-in-depth FE
|
||||||
|
// (BE cũ cache / deploy lệch vẫn không lòi nút). Dialog/isCancel giữ dead-safe.
|
||||||
|
const next = evaluation.workflow.nextPhases.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
|
||||||
const flow = evaluation.approvalFlow
|
const flow = evaluation.approvalFlow
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -136,7 +136,10 @@ export function PeWorkflowPanel({
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const next = evaluation.workflow.nextPhases
|
// UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi".
|
||||||
|
// BE policy đã gỡ TuChoi khỏi nextPhases; filter này = defense-in-depth FE
|
||||||
|
// (BE cũ cache / deploy lệch vẫn không lòi nút). Dialog/isCancel giữ dead-safe.
|
||||||
|
const next = evaluation.workflow.nextPhases.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
|
||||||
const flow = evaluation.approvalFlow
|
const flow = evaluation.approvalFlow
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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.
|
// 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.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
|
||||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing)] = [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.ChoCCM)] = [AppRoles.Procurement],
|
||||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
||||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
|
|
||||||
|
|
||||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
|
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
|
||||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [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.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
|
||||||
},
|
},
|
||||||
PhaseSla: DefaultSla,
|
PhaseSla: DefaultSla,
|
||||||
ActivePhases:
|
ActivePhases:
|
||||||
@ -94,30 +90,23 @@ public static class PurchaseEvaluationPolicies
|
|||||||
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
|
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
|
||||||
{
|
{
|
||||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
[(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.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.ChoDuAn)] = [AppRoles.Procurement],
|
||||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
||||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
|
|
||||||
|
|
||||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
|
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
|
||||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TraLai)] = [AppRoles.ProjectManager],
|
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TraLai)] = [AppRoles.ProjectManager],
|
||||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.ProjectManager],
|
|
||||||
|
|
||||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
|
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
|
||||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [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.ChoCEODuyetNCC)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||||
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.TraLai)] = [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.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
|
||||||
},
|
},
|
||||||
PhaseSla: DefaultSla,
|
PhaseSla: DefaultSla,
|
||||||
ActivePhases:
|
ActivePhases:
|
||||||
@ -160,24 +149,23 @@ public static class PurchaseEvaluationPolicyRegistry
|
|||||||
// Workflow chạy theo state machine 5 trạng thái + iterate Steps/Levels —
|
// 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
|
// 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:
|
// tự handle advance level/step bên trong ChoDuyet, FE chỉ cần biết:
|
||||||
// DangSoanThao/TraLai → ChoDuyet (trình) | TuChoi (huỷ)
|
// DangSoanThao/TraLai → ChoDuyet (trình)
|
||||||
// ChoDuyet → ChoDuyet (advance) | TraLai (trả lại) | TuChoi (từ chối)
|
// 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()
|
public static PurchaseEvaluationPolicy ForV2Schema()
|
||||||
{
|
{
|
||||||
var transitions = new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
|
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
|
// 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.ChoDuyet)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
|
||||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoDuyet)] = [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ỉ
|
// ChoDuyet — Service guard match approver ApproverUserId, Policy chỉ
|
||||||
// expose 3 nút cho FE (Duyệt forward / Trả lại / Từ chối). Roles "*"
|
// 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.
|
// để guard không block; Service tự enforce ApproverUserId match.
|
||||||
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.ChoDuyet)] = ["*"],
|
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.ChoDuyet)] = ["*"],
|
||||||
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TraLai)] = ["*"],
|
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TraLai)] = ["*"],
|
||||||
[(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TuChoi)] = ["*"],
|
|
||||||
};
|
};
|
||||||
var sla = new Dictionary<PurchaseEvaluationPhase, TimeSpan?>
|
var sla = new Dictionary<PurchaseEvaluationPhase, TimeSpan?>
|
||||||
{
|
{
|
||||||
@ -245,29 +233,21 @@ public static class PurchaseEvaluationPolicyRegistry
|
|||||||
userTransitions.TryAdd((PurchaseEvaluationPhase.TraLai, s.Phase), userIds);
|
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)
|
if (prev.Value != PurchaseEvaluationPhase.DangSoanThao && s.Phase != PurchaseEvaluationPhase.DangSoanThao)
|
||||||
{
|
{
|
||||||
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), roles);
|
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), roles);
|
||||||
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), roles);
|
|
||||||
if (userIds.Length > 0)
|
if (userIds.Length > 0)
|
||||||
{
|
{
|
||||||
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), userIds);
|
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), userIds);
|
||||||
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), userIds);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev = s.Phase;
|
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
|
// Terminal states always available + TraLai phase
|
||||||
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))
|
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))
|
||||||
|
|||||||
@ -71,6 +71,20 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
"(xem gotcha #45 + docs/workflow-contract.md).");
|
"(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) =====
|
// ===== REJECT BRANCH (extended Mig 28 — F1 multi-mode Trả lại) =====
|
||||||
if (decision == ApprovalDecision.Reject)
|
if (decision == ApprovalDecision.Reject)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -158,21 +158,51 @@ public class PurchaseEvaluationPolicyTests
|
|||||||
.Should().BeTrue();
|
.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi".
|
||||||
|
// Spec change: TuChoi GỠ khỏi MỌI transition map (trước đó Drafter/DeptManager
|
||||||
|
// được TuChoi từ Nháp/TraLai + approver TuChoi từ phase trung gian).
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
|
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
|
||||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccWithPlan))]
|
[InlineData(nameof(PurchaseEvaluationPolicies.NccWithPlan))]
|
||||||
public void BothPolicies_DangSoanThao_To_TuChoi_DrafterOrDeptManager(string policyName)
|
public void BothPolicies_TuChoi_RemovedFromAllTransitions_S60(string policyName)
|
||||||
{
|
{
|
||||||
var policy = policyName == nameof(PurchaseEvaluationPolicies.NccOnly)
|
var policy = policyName == nameof(PurchaseEvaluationPolicies.NccOnly)
|
||||||
? PurchaseEvaluationPolicies.NccOnly
|
? PurchaseEvaluationPolicies.NccOnly
|
||||||
: PurchaseEvaluationPolicies.NccWithPlan;
|
: PurchaseEvaluationPolicies.NccWithPlan;
|
||||||
|
|
||||||
|
// Không role nào TuChoi được nữa — kể cả Drafter/DeptManager (huỷ phiếu
|
||||||
|
// nháp = nút Xóa riêng) lẫn approver phase trung gian.
|
||||||
policy.IsTransitionAllowed(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi,
|
policy.IsTransitionAllowed(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi,
|
||||||
[AppRoles.Drafter])
|
[AppRoles.Drafter, AppRoles.DeptManager])
|
||||||
.Should().BeTrue();
|
.Should().BeFalse();
|
||||||
policy.IsTransitionAllowed(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi,
|
policy.IsTransitionAllowed(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi,
|
||||||
|
[AppRoles.Drafter, AppRoles.DeptManager])
|
||||||
|
.Should().BeFalse();
|
||||||
|
policy.IsTransitionAllowed(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi,
|
||||||
[AppRoles.Procurement])
|
[AppRoles.Procurement])
|
||||||
.Should().BeFalse();
|
.Should().BeFalse();
|
||||||
|
|
||||||
|
// NextPhases (nguồn nút FE) không bao giờ chứa TuChoi nữa.
|
||||||
|
foreach (var phase in policy.ActivePhases)
|
||||||
|
policy.NextPhasesFrom(phase).Should().NotContain(PurchaseEvaluationPhase.TuChoi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void V2SchemaPolicy_TuChoi_RemovedFromAllTransitions_S60()
|
||||||
|
{
|
||||||
|
var policy = PurchaseEvaluationPolicyRegistry.ForV2Schema();
|
||||||
|
|
||||||
|
policy.IsTransitionAllowed(PurchaseEvaluationPhase.ChoDuyet, PurchaseEvaluationPhase.TuChoi,
|
||||||
|
[AppRoles.Drafter, AppRoles.DeptManager, AppRoles.Director])
|
||||||
|
.Should().BeFalse();
|
||||||
|
foreach (var phase in policy.ActivePhases)
|
||||||
|
policy.NextPhasesFrom(phase).Should().NotContain(PurchaseEvaluationPhase.TuChoi);
|
||||||
|
|
||||||
|
// 2 hành động còn lại giữ nguyên: trình + duyệt/trả lại.
|
||||||
|
policy.NextPhasesFrom(PurchaseEvaluationPhase.DangSoanThao)
|
||||||
|
.Should().Contain(PurchaseEvaluationPhase.ChoDuyet);
|
||||||
|
policy.NextPhasesFrom(PurchaseEvaluationPhase.ChoDuyet)
|
||||||
|
.Should().Contain(PurchaseEvaluationPhase.TraLai);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Registry mapping per PEType =====
|
// ===== Registry mapping per PEType =====
|
||||||
|
|||||||
@ -121,6 +121,37 @@ public class PurchaseEvaluationWorkflowServiceGuardTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TransitionAsync_TargetTuChoi_WithRejectDecision_Throws_TuChoiRemoved_S60()
|
||||||
|
{
|
||||||
|
// UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả về
|
||||||
|
// thôi". Payload TuChoi + decision=Reject (hợp lệ THEO SPEC CŨ) giờ bị
|
||||||
|
// guard S60 chặn hẳn — TuChoi hết đường vào từ transition API (guard
|
||||||
|
// #45 decision-mismatch vẫn đứng trước cover nhánh TuChoi+Approve).
|
||||||
|
var (svc, fix, db) = CreateService();
|
||||||
|
using (fix)
|
||||||
|
{
|
||||||
|
var pe = BuildPeInChoDuyet("PE-GUARD-004");
|
||||||
|
db.PurchaseEvaluations.Add(pe);
|
||||||
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
var act = async () => await svc.TransitionAsync(
|
||||||
|
evaluation: pe,
|
||||||
|
targetPhase: PurchaseEvaluationPhase.TuChoi,
|
||||||
|
actorUserId: Guid.NewGuid(),
|
||||||
|
actorRoles: new[] { AppRoles.CostControl },
|
||||||
|
decision: ApprovalDecision.Reject,
|
||||||
|
comment: "test tu choi removed S60",
|
||||||
|
ct: CancellationToken.None);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<ConflictException>()
|
||||||
|
.WithMessage("*Từ chối*đã được gỡ*");
|
||||||
|
|
||||||
|
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
|
||||||
|
"Guard chặn trước khi mutate — phiếu giữ nguyên ChoDuyet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai()
|
public async Task TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user