[CLAUDE] Workflow: State machine 5 trạng thái — Trả lại = Phase riêng
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m17s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m17s
Session 17 spec: chốt 5 trạng thái phiếu PE/HĐ/Budget theo state diagram:
Nháp ─trình──► Đã gửi duyệt ─approve cấp cuối──► Đã duyệt (terminal)
├─ Trả lại ────────► Trả lại
└─ Từ chối ────────► Từ chối (terminal)
Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu)
Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại = Phase
RIÊNG (TraLai=98), không revert về DangSoanThao + không jump-back step.
Drafter từ TraLai gửi lại như case Nháp — workflow chạy lại từ Cấp 1
Bước 1 (Option A diagram chốt với user).
BE Domain:
- ContractPhase + TraLai = 98
- BudgetPhase + TraLai = 98
- PurchaseEvaluationPhase: TraLai=98 đổi từ [LEGACY deprecated] thành
primary state. Comment update enum docs cho cả 3.
BE Policy (PE/HĐ/Budget):
- Reject transitions trỏ về TraLai (thay DangSoanThao)
- Mirror entry transitions: TraLai → next phase (cho Drafter resubmit)
- ActivePhases thêm TraLai
- FromDefinition mirror: TraLai → step.Phase + reject → TraLai
- DefaultSla cho TraLai = same as DangSoanThao
BE Service (PE + Contract):
- Reject branch: target=TuChoi giữ; else set Phase=TraLai, clear
CurrentWorkflowStepIndex=null
- Bỏ ResumeAfterReject branch + RejectedAtStepIndex/RejectedFromPhase
assignment (DB column giữ deprecated cho data cũ)
- Drafter trình branch: từ DangSoanThao HOẶC TraLai → ChoDuyet, init
CurrentWorkflowStepIndex=0 (cùng entry point, chạy lại từ đầu)
- Notification: TraLai when fromPhase=ChoDuyet → "bị trả lại"
- Budget Handler: simplify reject → TraLai, bỏ smart-reject + isResuming
BE Tests update:
- WorkflowPolicyTests: Standard_RejectFromCCM → TraLai (rename + assert)
+ Standard_TraLai_To_DangGopY_Allowed_For_Drafter (new)
- PurchaseEvaluationPolicyTests: BothPolicies_RejectFromCCM → TraLai
+ BothPolicies_TraLai_To_ChoPurchasing_AllowedForDrafter (new theory)
- BudgetPolicyTests: Default_CostControl_ChoCCM_To_TraLai (rename)
+ ActivePhases All6States (was All5) + NextPhasesFrom_TraLai (new)
+ NextPhasesFrom_ChoCEO_Includes_DaDuyet_And_TraLai (rename)
- 77 → 81 test pass (+4 tests TraLai entry point)
FE rename "Bản nháp" → "Nháp" (cả 2 app + types):
- types/purchaseEvaluation.ts: PurchaseEvaluationPhaseLabel 1=Nháp,
10=Đã gửi duyệt. PeDisplayStatus.BanNhap → Nhap (key + value).
PhaseLabel/Color cho TraLai update active.
- types/contracts.ts: +ChoDuyet=10, +TraLai=98 const + label/color.
Phase 2 'Đang soạn thảo' → 'Nháp'.
- types/budget.ts: +TraLai=98 const + label/color. Phase 1 → 'Nháp'.
- PeListPanel + PurchaseEvaluationsListPage filter dropdown: Nhap +
TraLai map đúng phase value.
BE label maps update consistent:
- ContractExcelExporter PhaseLabel: DangSoanThao → "Nháp" + add ChoDuyet/
TraLai entries.
- PeWorkflowAdminFeatures + WorkflowAdminFeatures PhaseLabels: same.
Verify: dotnet test 81 pass · npm build × 2 app pass · BE 0 error.
Field RejectedAtStepIndex/RejectedFromPhase giữ DB column (nullable,
không set value mới). Cleanup migration sau.
This commit is contained in:
@ -136,29 +136,24 @@ 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) =====
|
||||
// ===== Reject → TraLai (Session 17 spec mới) =====
|
||||
// Bỏ smart-reject jump-back. Trả lại = Phase riêng (TraLai).
|
||||
// Drafter từ TraLai gửi lại như Nháp — Policy `(TraLai, ChoCCM)` đã wire.
|
||||
// Field RejectedFromPhase giữ DB column nhưng KHÔNG set value mới (data cũ vẫn đọc).
|
||||
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)
|
||||
if (request.Decision == ApprovalDecision.Reject && targetPhase != BudgetPhase.TuChoi)
|
||||
{
|
||||
entity.RejectedFromPhase = fromPhase;
|
||||
targetPhase = BudgetPhase.DangSoanThao;
|
||||
}
|
||||
else if (isResumingAfterReject)
|
||||
{
|
||||
targetPhase = entity.RejectedFromPhase!.Value;
|
||||
entity.RejectedFromPhase = null;
|
||||
// Trả lại — override target → TraLai
|
||||
targetPhase = BudgetPhase.TraLai;
|
||||
}
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
|
||||
|
||||
// Policy guard — bypass khi resume sau reject.
|
||||
if (!isAdmin && !isResumingAfterReject
|
||||
// Policy guard
|
||||
if (!isAdmin
|
||||
&& !policy.IsTransitionAllowed(fromPhase, targetPhase, currentUser.Roles))
|
||||
throw new ForbiddenException(
|
||||
$"Role không đủ quyền chuyển {fromPhase} → {targetPhase}.");
|
||||
@ -168,8 +163,8 @@ public class TransitionBudgetCommandHandler(
|
||||
// nhưng giữ consistent UX 3 module.
|
||||
if (request.Decision == ApprovalDecision.Approve
|
||||
&& targetPhase != BudgetPhase.DangSoanThao
|
||||
&& targetPhase != BudgetPhase.TraLai
|
||||
&& targetPhase != BudgetPhase.TuChoi
|
||||
&& !isResumingAfterReject
|
||||
&& !isAdmin
|
||||
&& currentUser.UserId is Guid actorUid)
|
||||
{
|
||||
|
||||
@ -74,7 +74,7 @@ public class GetWorkflowAdminOverviewQueryHandler(
|
||||
|
||||
private static readonly Dictionary<ContractPhase, string> PhaseLabels = new()
|
||||
{
|
||||
[ContractPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[ContractPhase.DangSoanThao] = "Nháp",
|
||||
[ContractPhase.DangGopY] = "Đang góp ý",
|
||||
[ContractPhase.DangDamPhan] = "Đang đàm phán",
|
||||
[ContractPhase.DangInKy] = "Đang in ký",
|
||||
@ -82,6 +82,9 @@ public class GetWorkflowAdminOverviewQueryHandler(
|
||||
[ContractPhase.DangTrinhKy] = "Đang trình ký",
|
||||
[ContractPhase.DangDongDau] = "Đang đóng dấu",
|
||||
[ContractPhase.DaPhatHanh] = "Đã phát hành",
|
||||
[ContractPhase.ChoDuyet] = "Đã gửi duyệt",
|
||||
[ContractPhase.TraLai] = "Trả lại",
|
||||
[ContractPhase.TuChoi] = "Từ chối",
|
||||
};
|
||||
|
||||
public async Task<WorkflowAdminOverviewDto> Handle(GetWorkflowAdminOverviewQuery request, CancellationToken ct)
|
||||
|
||||
@ -69,13 +69,16 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
|
||||
private static readonly Dictionary<PurchaseEvaluationPhase, string> PhaseLabels = new()
|
||||
{
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = "Nháp",
|
||||
[PurchaseEvaluationPhase.ChoPurchasing] = "Chờ Purchasing",
|
||||
[PurchaseEvaluationPhase.ChoDuAn] = "Chờ Dự án",
|
||||
[PurchaseEvaluationPhase.ChoCCM] = "Chờ CCM",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetPA] = "Chờ CEO duyệt PA",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetNCC] = "Chờ CEO duyệt NCC",
|
||||
[PurchaseEvaluationPhase.ChoDuyet] = "Đã gửi duyệt",
|
||||
[PurchaseEvaluationPhase.DaDuyet] = "Đã duyệt",
|
||||
[PurchaseEvaluationPhase.TraLai] = "Trả lại",
|
||||
[PurchaseEvaluationPhase.TuChoi] = "Từ chối",
|
||||
};
|
||||
|
||||
public async Task<PeWorkflowAdminOverviewDto> Handle(GetPeWorkflowAdminOverviewQuery request, CancellationToken ct)
|
||||
|
||||
Reference in New Issue
Block a user