[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

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:
pqhuy1987
2026-05-08 14:12:38 +07:00
parent f3bea3c616
commit ff21120c8c
25 changed files with 286 additions and 187 deletions

View File

@ -1,14 +1,17 @@
namespace SolutionErp.Domain.Budgets;
// State machine ngân sách — đơn giản 3 bước duyệt + 2 terminal.
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet
// Bất kỳ phase duyệt → DangSoanThao (reject)
// DangSoanThao → TuChoi
// State machine ngân sách — Session 17 spec mới (5 trạng thái mirror PE/HĐ):
// DangSoanThao (Nháp) → ChoCCM (Drafter trình)
// TraLai (Trả lại) → ChoCCM (Drafter sửa+gửi lại, chạy từ đầu)
// ChoCCM/ChoCEO → next phase OR TraLai OR TuChoi
// ChoCEO → DaDuyet (terminal)
// DangSoanThao/TraLai → TuChoi (Drafter huỷ)
public enum BudgetPhase
{
DangSoanThao = 1,
ChoCCM = 2,
ChoCEO = 3,
DaDuyet = 4,
TraLai = 98, // Phase riêng (không revert DangSoanThao)
TuChoi = 99,
}

View File

@ -31,12 +31,15 @@ public static class BudgetPolicies
private static readonly Dictionary<BudgetPhase, TimeSpan?> DefaultSla = new()
{
[BudgetPhase.DangSoanThao] = TimeSpan.FromDays(5),
[BudgetPhase.TraLai] = TimeSpan.FromDays(5),
[BudgetPhase.ChoCCM] = TimeSpan.FromDays(3),
[BudgetPhase.ChoCEO] = TimeSpan.FromDays(2),
[BudgetPhase.DaDuyet] = null,
[BudgetPhase.TuChoi] = null,
};
// Session 17 spec: Reject = về TraLai (Phase riêng). Drafter từ TraLai
// gửi lại = entry point thứ 2 (mirror DangSoanThao → ChoCCM).
public static readonly BudgetPolicy Default = new(
Name: "Default",
Description: "Quy trình ngân sách 3-step (Drafter → CCM → CEO).",
@ -44,17 +47,22 @@ public static class BudgetPolicies
{
[(BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.DangSoanThao, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.TraLai, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.TraLai, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCCM, BudgetPhase.TraLai)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCCM, BudgetPhase.TuChoi)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(BudgetPhase.ChoCEO, BudgetPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(BudgetPhase.ChoCEO, BudgetPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(BudgetPhase.ChoCEO, BudgetPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
BudgetPhase.DangSoanThao,
BudgetPhase.TraLai,
BudgetPhase.ChoCCM,
BudgetPhase.ChoCEO,
BudgetPhase.DaDuyet,

View File

@ -1,25 +1,28 @@
namespace SolutionErp.Domain.Contracts;
// State machine HĐ — Session 16 drastic refactor (Mig 21):
// DangSoanThao → ChoDuyet (Drafter trình, init CurrentWorkflowStepIndex=0)
// ChoDuyet → ChoDuyet (advance step pointer per approve)
// ChoDuyet → DaPhatHanh (last step done — terminal)
// ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex, Drafter sửa)
// ChoDuyet → TuChoi (Từ chối — terminal khoá)
// State machine HĐ — Session 17 spec mới (5 trạng thái):
// DangSoanThao (Nháp) ──Drafter trình──► ChoDuyet
// TraLai (Trả lại) ──Drafter sửa+gửi lại──► ChoDuyet (chạy lại từ Cấp 1 Bước 1)
// ChoDuyet (Đã gửi duyệt) ──advance step pointer──► ChoDuyet
// ──last step done──────────► DaPhatHanh (terminal + gen mã HĐ)
// ──Approver Trả lại────────► TraLai
// ──Approver Từ chối────────► TuChoi (terminal)
//
// LEGACY values (DangChon, DangGopY, DangDamPhan, DangInKy, DangKiemTraCCM,
// DangTrinhKy, DangDongDau) deprecated post-Mig 21 — giữ enum cho data cũ.
// TraLai=98: Session 17 thêm mới — Trả lại là Phase RIÊNG (mirror PE).
public enum ContractPhase
{
DangChon = 1, // [LEGACY]
DangSoanThao = 2,
DangSoanThao = 2, // Nháp
DangGopY = 3, // [LEGACY]
DangDamPhan = 4, // [LEGACY]
DangInKy = 5, // [LEGACY]
DangKiemTraCCM = 6, // [LEGACY]
DangTrinhKy = 7, // [LEGACY]
DangDongDau = 8, // [LEGACY]
DaPhatHanh = 9, // terminal thành công (= DaDuyet cho HĐ)
ChoDuyet = 10, // [Mig 21] generic intermediate, dùng CurrentWorkflowStepIndex tracking
TuChoi = 99, // terminal khoá
DaPhatHanh = 9, // Đã duyệt (cho HĐ — gen mã + phát hành) — terminal thành công
ChoDuyet = 10, // Đã gửi duyệt — generic intermediate, CurrentWorkflowStepIndex tracking
TraLai = 98, // Trả lại — Phase riêng, Drafter sửa rồi gửi lại chạy từ đầu
TuChoi = 99, // Từ chối — terminal khoá
}

View File

@ -52,6 +52,7 @@ public static class WorkflowPolicies
private static readonly Dictionary<ContractPhase, TimeSpan?> DefaultSla = new()
{
[ContractPhase.DangSoanThao] = TimeSpan.FromDays(7),
[ContractPhase.TraLai] = TimeSpan.FromDays(7),
[ContractPhase.DangGopY] = TimeSpan.FromDays(7),
[ContractPhase.DangDamPhan] = TimeSpan.FromDays(7),
[ContractPhase.DangInKy] = TimeSpan.FromDays(1),
@ -65,6 +66,8 @@ public static class WorkflowPolicies
// ===== STANDARD: 9-phase formal workflow =====
// Per QT-TP-NCC.docx: Thầu phụ / NCC / Tổ đội — full CCM review required.
// Session 17: Reject = về TraLai (Phase riêng). Drafter từ TraLai gửi lại
// = entry point thứ 2 (mirror DangSoanThao → DangGopY).
public static readonly WorkflowPolicy Standard = new(
Name: "Standard",
Description: "Quy trình đầy đủ 8 phase — CCM kiểm tra + BOD duyệt. Áp dụng HĐ Thầu phụ / NCC / Giao khoán.",
@ -72,26 +75,28 @@ public static class WorkflowPolicies
{
[(ContractPhase.DangSoanThao, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangSoanThao, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.TraLai, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.TraLai, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangGopY, ContractPhase.DangDamPhan)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangGopY, ContractPhase.DangSoanThao)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
[(ContractPhase.DangGopY, ContractPhase.TraLai)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
[(ContractPhase.DangDamPhan, ContractPhase.DangInKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
[(ContractPhase.DangInKy, ContractPhase.DangKiemTraCCM)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
[(ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy)] = [AppRoles.CostControl],
[(ContractPhase.DangKiemTraCCM, ContractPhase.DangSoanThao)] = [AppRoles.CostControl],
[(ContractPhase.DangKiemTraCCM, ContractPhase.TraLai)] = [AppRoles.CostControl],
[(ContractPhase.DangTrinhKy, ContractPhase.DangDongDau)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangTrinhKy, ContractPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangTrinhKy, ContractPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangDongDau, ContractPhase.DaPhatHanh)] = [AppRoles.HrAdmin],
},
PhaseSla: DefaultSla,
ActivePhases:
[
ContractPhase.DangSoanThao, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
ContractPhase.DangSoanThao, ContractPhase.TraLai, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
ContractPhase.DangInKy, ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy,
ContractPhase.DangDongDau, ContractPhase.DaPhatHanh, ContractPhase.TuChoi,
]);
@ -106,9 +111,11 @@ public static class WorkflowPolicies
{
[(ContractPhase.DangSoanThao, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangSoanThao, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.TraLai, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.TraLai, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangGopY, ContractPhase.DangDamPhan)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(ContractPhase.DangGopY, ContractPhase.DangSoanThao)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
[(ContractPhase.DangGopY, ContractPhase.TraLai)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
[(ContractPhase.DangDamPhan, ContractPhase.DangInKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
@ -116,14 +123,14 @@ public static class WorkflowPolicies
[(ContractPhase.DangInKy, ContractPhase.DangTrinhKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
[(ContractPhase.DangTrinhKy, ContractPhase.DangDongDau)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangTrinhKy, ContractPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangTrinhKy, ContractPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(ContractPhase.DangDongDau, ContractPhase.DaPhatHanh)] = [AppRoles.HrAdmin],
},
PhaseSla: DefaultSla,
ActivePhases:
[
ContractPhase.DangSoanThao, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
ContractPhase.DangSoanThao, ContractPhase.TraLai, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
ContractPhase.DangInKy, ContractPhase.DangTrinhKy,
ContractPhase.DangDongDau, ContractPhase.DaPhatHanh, ContractPhase.TuChoi,
]);
@ -213,22 +220,35 @@ public static class WorkflowPolicyRegistry
transitions[(prev.Value, s.Phase)] = roles;
if (userIds.Length > 0) userTransitions[(prev.Value, s.Phase)] = userIds;
// Reject path back to Drafter (common pattern QT docx)
// Mirror: TraLai → s.Phase (Drafter resubmit từ Trả lại = entry point thứ 2)
if (prev.Value == ContractPhase.DangSoanThao)
{
transitions.TryAdd((ContractPhase.TraLai, s.Phase), roles);
if (userIds.Length > 0)
userTransitions.TryAdd((ContractPhase.TraLai, s.Phase), userIds);
}
// Reject path → TraLai (Phase riêng, không revert DangSoanThao)
if (prev.Value != ContractPhase.DangSoanThao && s.Phase != ContractPhase.DangSoanThao)
{
transitions.TryAdd((s.Phase, ContractPhase.DangSoanThao), roles);
transitions.TryAdd((s.Phase, ContractPhase.TraLai), roles);
if (userIds.Length > 0)
userTransitions.TryAdd((s.Phase, ContractPhase.DangSoanThao), userIds);
userTransitions.TryAdd((s.Phase, ContractPhase.TraLai), userIds);
}
}
prev = s.Phase;
}
// First step có thể reject to TuChoi
// First step có thể reject to TuChoi (cả Nháp + Trả lại)
if (steps.Count > 0)
{
transitions.TryAdd((steps[0].Phase, ContractPhase.TuChoi),
[AppRoles.Drafter, AppRoles.DeptManager]);
transitions.TryAdd((ContractPhase.TraLai, ContractPhase.TuChoi),
[AppRoles.Drafter, AppRoles.DeptManager]);
}
if (!activePhases.Contains(ContractPhase.TuChoi)) activePhases.Add(ContractPhase.TuChoi);
if (!activePhases.Contains(ContractPhase.TraLai)) activePhases.Add(ContractPhase.TraLai);
return new WorkflowPolicy(
Name: $"{def.Code}-v{def.Version:D2}",

View File

@ -1,25 +1,27 @@
namespace SolutionErp.Domain.PurchaseEvaluations;
// State machine PE — Session 16 drastic refactor (Mig 21):
// DangSoanThao → ChoDuyet (Drafter trình, init CurrentWorkflowStepIndex=0)
// ChoDuyet → ChoDuyet (advance step pointer mỗi lần approve)
// ChoDuyet → DaDuyet (last step done — terminal thành công)
// ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex, Drafter sửa)
// ChoDuyet → TuChoi (Từ chối — terminal khoá phiếu)
// State machine PE — Session 17 spec mới (5 trạng thái):
// DangSoanThao (Nháp) ──Drafter trình──► ChoDuyet
// TraLai (Trả lại) ──Drafter sửa+gửi lại──► ChoDuyet (chạy lại từ Cấp 1 Bước 1)
// ChoDuyet (Đã gửi duyệt) ──advance step pointer──► ChoDuyet
// ──last step done──────────► DaDuyet (terminal)
// ──Approver Trả lại────────► TraLai
// ──Approver Từ chối────────► TuChoi (terminal)
// DangSoanThao → TuChoi (Drafter huỷ trước trình)
//
// LEGACY values 2-6 + 98 deprecated post-Mig 21 (data cũ vẫn đọc OK,
// new workflow definitions chỉ dùng DangSoanThao/ChoDuyet/DaDuyet/TuChoi).
// LEGACY values 2-6 deprecated post-Mig 21 (data cũ vẫn đọc OK).
// TraLai=98: Session 17 restore làm primary state — Trả lại là Phase RIÊNG,
// không revert về DangSoanThao như Mig 21.
public enum PurchaseEvaluationPhase
{
DangSoanThao = 1,
ChoPurchasing = 2, // [LEGACY] deprecated
ChoDuAn = 3, // [LEGACY] deprecated
ChoCCM = 4, // [LEGACY] deprecated
ChoCEODuyetPA = 5, // [LEGACY] deprecated
ChoCEODuyetNCC = 6, // [LEGACY] deprecated
DaDuyet = 7, // terminal thành công
ChoDuyet = 10, // [Mig 21] generic intermediate, dùng CurrentWorkflowStepIndex tracking
TraLai = 98, // [LEGACY] deprecated — Session 14 chốt thay bằng Trả lại = về DangSoanThao
TuChoi = 99, // terminal từ chối — KHÔNG cho edit/thao tác
DangSoanThao = 1, // Nháp
ChoPurchasing = 2, // [LEGACY] deprecated
ChoDuAn = 3, // [LEGACY] deprecated
ChoCCM = 4, // [LEGACY] deprecated
ChoCEODuyetPA = 5, // [LEGACY] deprecated
ChoCEODuyetNCC = 6, // [LEGACY] deprecated
DaDuyet = 7, // Đã duyệt — terminal thành công
ChoDuyet = 10, // Đã gửi duyệt — generic intermediate, CurrentWorkflowStepIndex tracking
TraLai = 98, // Trả lại — Phase riêng, Drafter sửa rồi gửi lại chạy từ đầu
TuChoi = 99, // Từ chối — terminal khoá phiếu, KHÔNG cho edit/thao tác
}

View File

@ -38,6 +38,7 @@ public static class PurchaseEvaluationPolicies
private static readonly Dictionary<PurchaseEvaluationPhase, TimeSpan?> DefaultSla = new()
{
[PurchaseEvaluationPhase.DangSoanThao] = TimeSpan.FromDays(3),
[PurchaseEvaluationPhase.TraLai] = TimeSpan.FromDays(3),
[PurchaseEvaluationPhase.ChoPurchasing] = TimeSpan.FromDays(2),
[PurchaseEvaluationPhase.ChoDuAn] = TimeSpan.FromDays(2),
[PurchaseEvaluationPhase.ChoCCM] = TimeSpan.FromDays(2),
@ -48,33 +49,37 @@ public static class PurchaseEvaluationPolicies
};
// A — DuyetNcc (3 step thực + Drafter soạn): Drafter → Purchasing → CCM → CEO
// Session 17 spec: Reject = về TraLai (Phase riêng, không revert DangSoanThao).
// Drafter từ TraLai gửi lại = entry point thứ 2 (mirror DangSoanThao → ChoPurchasing).
public static readonly PurchaseEvaluationPolicy NccOnly = new(
Name: "NccOnly",
Description: "Duyệt NCC — 3 step (Purchasing → CCM → CEO). Không cần duyệt phương án.",
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.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 Drafter (DangSoanThao) / Từ chối hoàn toàn (TuChoi).
// Trả lại = smart reject pattern Mig 16 (set RejectedFromPhase + về DangSoanThao + Drafter sửa).
// Từ chối = phiếu khoá hoàn toàn (Phase=TuChoi → 17 handler Mig 16 lock edit).
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai Phase riêng) / Từ chối (TuChoi terminal).
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [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.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
PurchaseEvaluationPhase.DangSoanThao,
PurchaseEvaluationPhase.TraLai,
PurchaseEvaluationPhase.ChoPurchasing,
PurchaseEvaluationPhase.ChoCCM,
PurchaseEvaluationPhase.ChoCEODuyetNCC,
@ -90,32 +95,35 @@ public static class PurchaseEvaluationPolicies
{
[(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 / Từ chối (xem comment NccOnly).
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai) / Từ chối (TuChoi).
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TraLai)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [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.DangSoanThao)] = [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.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
PurchaseEvaluationPhase.DangSoanThao,
PurchaseEvaluationPhase.TraLai,
PurchaseEvaluationPhase.ChoPurchasing,
PurchaseEvaluationPhase.ChoDuAn,
PurchaseEvaluationPhase.ChoCCM,
@ -183,30 +191,45 @@ public static class PurchaseEvaluationPolicyRegistry
transitions[(prev.Value, s.Phase)] = roles;
if (userIds.Length > 0) userTransitions[(prev.Value, s.Phase)] = userIds;
// 3 hành động phase trung gian — Duyệt forward + Trả lại Drafter + Từ chối hoàn toàn
// Mirror: TraLai → s.Phase cho cả Drafter (resubmit từ Trả lại = entry point thứ 2)
if (prev.Value == PurchaseEvaluationPhase.DangSoanThao)
{
transitions.TryAdd((PurchaseEvaluationPhase.TraLai, s.Phase), roles);
if (userIds.Length > 0)
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)
if (prev.Value != PurchaseEvaluationPhase.DangSoanThao && s.Phase != PurchaseEvaluationPhase.DangSoanThao)
{
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), roles);
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), roles);
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), roles);
if (userIds.Length > 0)
{
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), userIds);
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)
// 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
// Terminal states always available + TraLai phase
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))
activePhases.Add(PurchaseEvaluationPhase.TuChoi);
if (!activePhases.Contains(PurchaseEvaluationPhase.DaDuyet))
activePhases.Add(PurchaseEvaluationPhase.DaDuyet);
if (!activePhases.Contains(PurchaseEvaluationPhase.TraLai))
activePhases.Add(PurchaseEvaluationPhase.TraLai);
return new PurchaseEvaluationPolicy(
Name: $"{def.Code}-v{def.Version:D2}",