[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:
@ -12,7 +12,7 @@ public class ContractExcelExporter(IApplicationDbContext db, IDateTime dateTime)
|
||||
private static readonly Dictionary<ContractPhase, string> PhaseLabel = new()
|
||||
{
|
||||
[ContractPhase.DangChon] = "Đang chọn NCC",
|
||||
[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ý",
|
||||
@ -20,6 +20,8 @@ public class ContractExcelExporter(IApplicationDbContext db, IDateTime dateTime)
|
||||
[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",
|
||||
};
|
||||
|
||||
|
||||
@ -11,10 +11,15 @@ using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// Contract Workflow Service — Session 16 drastic refactor (Mig 21):
|
||||
// Flat workflow model. Mỗi step = 1 (Phòng × Cấp + Approvers). Service iterate
|
||||
// steps OrderBy Order, advance Contract.CurrentWorkflowStepIndex per approve.
|
||||
// Phase enum simplified: DangSoanThao → ChoDuyet → DaPhatHanh / TuChoi.
|
||||
// Contract Workflow Service — Session 17 spec mới (state machine 5 trạng thái):
|
||||
// Nháp (DangSoanThao) ──trình──► Đã gửi duyệt (ChoDuyet) ─approve cấp cuối─► Đã phát hành (DaPhatHanh, terminal + gen mã)
|
||||
// ├─ Trả lại ────────► Trả lại (TraLai)
|
||||
// └─ Từ chối ────────► Từ chối (TuChoi, terminal)
|
||||
// Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu, KHÔNG jump-back)
|
||||
//
|
||||
// Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại = Phase RIÊNG
|
||||
// (TraLai=98), không revert về DangSoanThao. Drafter từ TraLai gửi lại như
|
||||
// case Nháp — workflow chạy lại từ Cấp 1 Bước 1.
|
||||
public class ContractWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IContractCodeGenerator codeGenerator,
|
||||
@ -48,9 +53,9 @@ public class ContractWorkflowService(
|
||||
}
|
||||
else
|
||||
{
|
||||
contract.RejectedFromPhase = fromPhase;
|
||||
contract.RejectedAtStepIndex = contract.CurrentWorkflowStepIndex;
|
||||
contract.Phase = ContractPhase.DangSoanThao;
|
||||
// Trả lại — Phase=TraLai RIÊNG (không revert về DangSoanThao).
|
||||
// Drafter sửa từ TraLai rồi gửi lại sẽ chạy lại từ Cấp 1 Bước 1.
|
||||
contract.Phase = ContractPhase.TraLai;
|
||||
contract.CurrentWorkflowStepIndex = null;
|
||||
}
|
||||
contract.SlaDeadline = null;
|
||||
@ -59,25 +64,9 @@ public class ContractWorkflowService(
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== RESUME AFTER REJECT =====
|
||||
var isResumingAfterReject = decision == ApprovalDecision.Approve
|
||||
&& fromPhase == ContractPhase.DangSoanThao
|
||||
&& contract.RejectedAtStepIndex != null;
|
||||
|
||||
if (isResumingAfterReject)
|
||||
{
|
||||
contract.Phase = ContractPhase.ChoDuyet;
|
||||
contract.CurrentWorkflowStepIndex = contract.RejectedAtStepIndex;
|
||||
contract.RejectedAtStepIndex = null;
|
||||
contract.RejectedFromPhase = null;
|
||||
contract.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||
await LogTransitionAsync(contract, fromPhase, ContractPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== DRAFTER TRÌNH =====
|
||||
if (fromPhase == ContractPhase.DangSoanThao
|
||||
// ===== DRAFTER TRÌNH/GỬI LẠI (Nháp HOẶC Trả lại → ChoDuyet) =====
|
||||
// Cả 2 entry point cùng logic: chạy lại từ đầu (CurrentWorkflowStepIndex=0).
|
||||
if ((fromPhase == ContractPhase.DangSoanThao || fromPhase == ContractPhase.TraLai)
|
||||
&& (targetPhase == ContractPhase.ChoDuyet || (!isAdmin && !isSystem)))
|
||||
{
|
||||
if (!isAdmin && !isSystem
|
||||
@ -213,7 +202,7 @@ public class ContractWorkflowService(
|
||||
NotificationType.ContractPublished),
|
||||
ContractPhase.TuChoi => ($"HĐ {contract.TenHopDong ?? "của bạn"} bị từ chối",
|
||||
NotificationType.ContractRejected),
|
||||
ContractPhase.DangSoanThao when fromPhase == ContractPhase.ChoDuyet =>
|
||||
ContractPhase.TraLai when fromPhase == ContractPhase.ChoDuyet =>
|
||||
($"HĐ {contract.TenHopDong ?? "của bạn"} bị trả lại — vui lòng sửa và trình lại",
|
||||
NotificationType.ContractRejected),
|
||||
_ => ($"HĐ {contract.TenHopDong ?? contract.MaHopDong ?? ""} chuyển phase mới",
|
||||
|
||||
@ -12,11 +12,17 @@ using SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// PE Workflow Service — Session 16 drastic refactor (Mig 21):
|
||||
// Flat workflow model. Mỗi step = 1 (Phòng × Cấp + Approvers). Service iterate
|
||||
// steps OrderBy Order, advance PE.CurrentWorkflowStepIndex per approve.
|
||||
// Phase enum simplified: DangSoanThao → ChoDuyet (active workflow) → DaDuyet
|
||||
// (terminal) / TuChoi (khoá). Trả lại = về DangSoanThao + save RejectedAtStepIndex.
|
||||
// PE Workflow Service — Session 17 spec mới (state machine 5 trạng thái):
|
||||
// Nháp (DangSoanThao) ──trình──► Đã gửi duyệt (ChoDuyet) ─approve cấp cuối─► Đã duyệt (DaDuyet, terminal)
|
||||
// ├─ Trả lại ────────► Trả lại (TraLai)
|
||||
// └─ Từ chối ────────► Từ chối (TuChoi, terminal)
|
||||
// Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu, KHÔNG jump-back)
|
||||
//
|
||||
// Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại giờ là Phase
|
||||
// RIÊNG (TraLai=98), không revert về DangSoanThao. Drafter từ TraLai gửi lại
|
||||
// như case Nháp — workflow chạy lại từ Cấp 1 Bước 1. Field RejectedAtStepIndex
|
||||
// + RejectedFromPhase giữ DB column (nullable, không set value mới) cho data
|
||||
// cũ — sẽ cleanup migration sau.
|
||||
public class PurchaseEvaluationWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IDateTime dateTime,
|
||||
@ -49,10 +55,9 @@ public class PurchaseEvaluationWorkflowService(
|
||||
}
|
||||
else
|
||||
{
|
||||
// Trả lại — về DangSoanThao + save RejectedAtStepIndex (resume jump-back).
|
||||
evaluation.RejectedFromPhase = fromPhase;
|
||||
evaluation.RejectedAtStepIndex = evaluation.CurrentWorkflowStepIndex;
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DangSoanThao;
|
||||
// Trả lại — Phase=TraLai RIÊNG (không revert về DangSoanThao).
|
||||
// Drafter sửa từ TraLai rồi gửi lại sẽ chạy lại từ Cấp 1 Bước 1.
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
}
|
||||
evaluation.SlaDeadline = null;
|
||||
@ -61,25 +66,10 @@ public class PurchaseEvaluationWorkflowService(
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== RESUME AFTER REJECT (Drafter trình lại) =====
|
||||
var isResumingAfterReject = decision == ApprovalDecision.Approve
|
||||
&& fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
&& evaluation.RejectedAtStepIndex != null;
|
||||
|
||||
if (isResumingAfterReject)
|
||||
{
|
||||
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = evaluation.RejectedAtStepIndex;
|
||||
evaluation.RejectedAtStepIndex = null;
|
||||
evaluation.RejectedFromPhase = null;
|
||||
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||
await LogTransitionAsync(evaluation, fromPhase, PurchaseEvaluationPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== DRAFTER TRÌNH (DangSoanThao → ChoDuyet) =====
|
||||
if (fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
// ===== DRAFTER TRÌNH/GỬI LẠI (Nháp HOẶC Trả lại → ChoDuyet) =====
|
||||
// Cả 2 entry point cùng logic: chạy lại từ đầu (CurrentWorkflowStepIndex=0).
|
||||
if ((fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
|| fromPhase == PurchaseEvaluationPhase.TraLai)
|
||||
&& (targetPhase == PurchaseEvaluationPhase.ChoDuyet || !isAdmin && !isSystem))
|
||||
{
|
||||
// Drafter/DeptManager only (or Admin bypass).
|
||||
@ -230,7 +220,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
NotificationType.ContractPublished),
|
||||
PurchaseEvaluationPhase.TuChoi => ($"Phiếu {evaluation.TenGoiThau} bị từ chối",
|
||||
NotificationType.ContractRejected),
|
||||
PurchaseEvaluationPhase.DangSoanThao when fromPhase == PurchaseEvaluationPhase.ChoDuyet =>
|
||||
PurchaseEvaluationPhase.TraLai when fromPhase == PurchaseEvaluationPhase.ChoDuyet =>
|
||||
($"Phiếu {evaluation.TenGoiThau} bị trả lại — vui lòng sửa và trình lại",
|
||||
NotificationType.ContractRejected),
|
||||
_ => ($"Phiếu {evaluation.TenGoiThau} chuyển phase mới",
|
||||
|
||||
Reference in New Issue
Block a user