[CLAUDE] Infra+App: Chunk C — Smart reject + Resume after reject (3 module)
Ràng buộc 2 (Phase 9): khi reject, trả về Drafter (DangSoanThao) + lưu phase nguồn. Drafter sửa lại + trình lại → quay về phase đã reject (skip phase trung gian). Logic flow: 1. Reject (Decision=Reject): - entity.RejectedFromPhase = currentPhase // snapshot phase đang reject - targetPhase override = DangSoanThao // force về Drafter - Approval row: FromPhase=X, ToPhase=DangSoanThao, Decision=Reject - Notification cho Drafter 2. Resume after reject (Decision=Approve, fromPhase=DangSoanThao, RejectedFromPhase != null): - targetPhase override = entity.RejectedFromPhase!.Value - entity.RejectedFromPhase = null // clear field - Skip policy guard (Drafter có quyền trình lại sau khi sửa) - Approval row: FromPhase=DangSoanThao, ToPhase=ResumePhase, Decision=Approve 3. Normal transition (chưa reject hoặc đã clear): - Logic cũ giữ nguyên — policy guard check + transition Pattern unified cho 3 module: - ContractWorkflowService.TransitionAsync: 2 case detect + override - PurchaseEvaluationWorkflowService.TransitionAsync: tương tự - TransitionBudgetCommandHandler.Handle: tương tự (Budget không có service riêng, logic ở handler) Files: - src/Backend/SolutionErp.Infrastructure/Services/ContractWorkflowService.cs - src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs - src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs Verify: - Build pass (2 warning DocxRenderer cũ, không liên quan) - 77 unit test pass — Domain policy không đổi, tests giữ nguyên Note: Approval history giờ track đầy đủ cycle reject→sửa→resume: Approval 1: DangGopY → DangSoanThao, Decision=Reject (CCM reject) Approval 2: DangSoanThao → DangGopY, Decision=Approve (Drafter resume) UI có thể detect "đã từng reject" qua RejectedFromPhase != null hoặc qua Approval history (Decision=Reject row gần nhất). Hiển thị banner đỏ "Phiếu đã bị reject từ phase X, lý do: Y" cho Drafter. Smart reject hoàn tất Ràng buộc 2. Còn Ràng buộc 3 (2-stage dept approval) ở Chunk D. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -131,24 +131,43 @@ public class TransitionBudgetCommandHandler(
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
// ===== Smart reject + resume (Phase 9 — Migration 16) =====
|
||||
var fromPhase = entity.Phase;
|
||||
var targetPhase = request.TargetPhase;
|
||||
var isResumingAfterReject = request.Decision == ApprovalDecision.Approve
|
||||
&& fromPhase == BudgetPhase.DangSoanThao
|
||||
&& entity.RejectedFromPhase != null;
|
||||
|
||||
if (request.Decision == ApprovalDecision.Reject)
|
||||
{
|
||||
entity.RejectedFromPhase = fromPhase;
|
||||
targetPhase = BudgetPhase.DangSoanThao;
|
||||
}
|
||||
else if (isResumingAfterReject)
|
||||
{
|
||||
targetPhase = entity.RejectedFromPhase!.Value;
|
||||
entity.RejectedFromPhase = null;
|
||||
}
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
|
||||
|
||||
if (!isAdmin && !policy.IsTransitionAllowed(entity.Phase, request.TargetPhase, currentUser.Roles))
|
||||
// Policy guard — bypass khi resume sau reject.
|
||||
if (!isAdmin && !isResumingAfterReject
|
||||
&& !policy.IsTransitionAllowed(fromPhase, targetPhase, currentUser.Roles))
|
||||
throw new ForbiddenException(
|
||||
$"Role không đủ quyền chuyển {entity.Phase} → {request.TargetPhase}.");
|
||||
$"Role không đủ quyền chuyển {fromPhase} → {targetPhase}.");
|
||||
|
||||
var fromPhase = entity.Phase;
|
||||
entity.SlaWarningSent = false;
|
||||
entity.Phase = request.TargetPhase;
|
||||
var sla = policy.PhaseSla.GetValueOrDefault(request.TargetPhase);
|
||||
entity.Phase = targetPhase;
|
||||
var sla = policy.PhaseSla.GetValueOrDefault(targetPhase);
|
||||
entity.SlaDeadline = sla is null ? null : DateTime.UtcNow.Add(sla.Value);
|
||||
|
||||
db.BudgetApprovals.Add(new BudgetApproval
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
FromPhase = fromPhase,
|
||||
ToPhase = request.TargetPhase,
|
||||
ToPhase = targetPhase,
|
||||
ApproverUserId = currentUser.UserId,
|
||||
Decision = request.Decision,
|
||||
Comment = request.Comment,
|
||||
@ -166,10 +185,10 @@ public class TransitionBudgetCommandHandler(
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Workflow,
|
||||
Action = ChangelogAction.Transition,
|
||||
PhaseAtChange = request.TargetPhase,
|
||||
PhaseAtChange = targetPhase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = actorName ?? "Hệ thống",
|
||||
Summary = $"Chuyển phase {fromPhase} → {request.TargetPhase}",
|
||||
Summary = $"Chuyển phase {fromPhase} → {targetPhase}",
|
||||
ContextNote = request.Comment,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
Reference in New Issue
Block a user