[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

@ -158,21 +158,51 @@ public class PurchaseEvaluationPolicyTests
.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]
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
[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)
? PurchaseEvaluationPolicies.NccOnly
: 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,
[AppRoles.Drafter])
.Should().BeTrue();
policy.IsTransitionAllowed(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi,
[AppRoles.Drafter, AppRoles.DeptManager])
.Should().BeFalse();
policy.IsTransitionAllowed(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi,
[AppRoles.Drafter, AppRoles.DeptManager])
.Should().BeFalse();
policy.IsTransitionAllowed(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi,
[AppRoles.Procurement])
.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 =====

View File

@ -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]
public async Task TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai()
{