[CLAUDE] Infra: Chunk D — PE 2-stage dept approval (đóng bug anh Kiệt báo)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m22s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m22s
Ràng buộc 3 (Phase 9) scope tối giản: chỉ PE workflow trước. Đóng bug
"NV duyệt được hết phase" anh Kiệt báo trong chat FDC.
Logic 2-stage trong PurchaseEvaluationWorkflowService.TransitionAsync,
chèn sau policy guard, trước phase transition:
1. Detect approving phase với role thuộc phòng ban:
- decision == Approve
- target != DangSoanThao && != TuChoi
- Không reject + không resume + không admin/system
- actorUserId != null + actor.DepartmentId != null
2. Stage detection:
- DeptManager (TPB) → Stage=Confirm trực tiếp (TPB tự confirm được)
- User.CanBypassReview=true → Stage=Confirm + IsBypassed=true (NV bypass)
- Else (NV thường) → Stage=Review only
3. Upsert PurchaseEvaluationDepartmentApproval row:
- UNIQUE (PEId, PhaseAtApproval, DepartmentId, Stage) đảm bảo 1 row
- UPDATE in-place khi user click Duyệt lần 2 (đổi comment)
- ApproverRoleSnapshot: "TPB" / "NV(bypass)" / "NV" denorm cho audit
4. Check Stage=Confirm tồn tại cho (PEId, fromPhase, deptId):
- hasConfirm = vừa insert Stage=Confirm OR đã có sẵn
- !hasConfirm → BLOCK phase transition:
* Insert PEApproval row (FromPhase=ToPhase=fromPhase, Decision=Approve,
Comment="[Review NV] ...") để track audit
* Insert Changelog "NV X đã review phase Y, chờ TPB confirm"
* Return early — Phase KHÔNG đổi
- hasConfirm → tiếp tục normal phase transition logic
5. Skip 2-stage hoàn toàn khi:
- Decision=Reject (smart reject Chunk C đã handle)
- Resume after reject (target đã pinned)
- Admin role hoặc System (auto-approve)
- actorUserId == null hoặc actor.DepartmentId == null
Bug fix verified theo flow anh Kiệt:
- User long.chau (NV.PRO, role=Procurement, DepartmentId=PRO) tạo phiếu
- long.chau click Duyệt phase ChoPurchasing → ChoCCM:
- actor.DepartmentId=PRO → 2-stage logic active
- role=Procurement, không có DeptManager → Stage=Review
- hasConfirm=false → BLOCK transition
- Insert PEDeptApproval(PE, ChoPurchasing, PRO, Review)
- Phase giữ nguyên ChoPurchasing
- TPB.PRO (tra.bui có role DeptManager + DeptId=PRO) click Duyệt:
- role=DeptManager → Stage=Confirm
- hasConfirm=true (vừa insert) → ALLOW transition
- Phase chuyển ChoPurchasing → ChoCCM
- NV CCM lặp pattern tương tự ở phase ChoCCM
- Cuối cùng CEO/AuthSigner duyệt ChoCEODuyetNCC → DaDuyet (CEO không thuộc
dept cụ thể nên bypass 2-stage)
Pending Chunk E:
- TODO notify TPB cùng dept khi NV review (best effort, chưa implement)
- List endpoint GET /api/purchase-evaluations/{id}/department-approvals
cho FE hiển thị progress 2-stage
- UserManager API PATCH /api/users/{id}/bypass-review
- FE Workflow Panel update + UserManager toggle
HĐ + Budget 2-stage scope sẽ làm sau khi PE verify UAT (per default chốt
trước đó).
Verify:
- Build pass (2 warning DocxRenderer cũ)
- 77 unit test pass — Domain policy chưa đụng
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -4,6 +4,7 @@ using SolutionErp.Application.Common.Exceptions;
|
|||||||
using SolutionErp.Application.Common.Interfaces;
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
using SolutionErp.Application.Notifications;
|
using SolutionErp.Application.Notifications;
|
||||||
using SolutionErp.Application.PurchaseEvaluations.Services;
|
using SolutionErp.Application.PurchaseEvaluations.Services;
|
||||||
|
using SolutionErp.Domain.Common;
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Identity;
|
using SolutionErp.Domain.Identity;
|
||||||
using SolutionErp.Domain.Notifications;
|
using SolutionErp.Domain.Notifications;
|
||||||
@ -85,6 +86,103 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 2-stage department approval (Phase 9 — Migration 16) =====
|
||||||
|
// Bug fix anh Kiệt: NV duyệt được hết phase. Logic mới:
|
||||||
|
// - User.DepartmentId != null + KHÔNG admin/system + KHÔNG resume:
|
||||||
|
// - DeptManager (TPB) → Stage=Confirm trực tiếp
|
||||||
|
// - CanBypassReview=true → Stage=Confirm + IsBypassed=true
|
||||||
|
// - Else (NV) → Stage=Review only, BLOCK phase transition cho đến khi TPB confirm
|
||||||
|
// - Skip với reject + resume + admin + system + actor không thuộc dept.
|
||||||
|
if (decision == ApprovalDecision.Approve
|
||||||
|
&& targetPhase != PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
&& targetPhase != PurchaseEvaluationPhase.TuChoi
|
||||||
|
&& !isResumingAfterReject
|
||||||
|
&& !isAdmin && !isSystem
|
||||||
|
&& actorUserId is Guid actorUid)
|
||||||
|
{
|
||||||
|
var actor = await userManager.FindByIdAsync(actorUid.ToString());
|
||||||
|
if (actor?.DepartmentId is Guid deptId)
|
||||||
|
{
|
||||||
|
var isManager = actorRoles.Contains(AppRoles.DeptManager);
|
||||||
|
var canBypass = actor.CanBypassReview;
|
||||||
|
var stage = (isManager || canBypass) ? ApprovalStage.Confirm : ApprovalStage.Review;
|
||||||
|
var isBypassed = !isManager && canBypass;
|
||||||
|
var roleSnapshot = isManager ? "TPB" : (canBypass ? "NV(bypass)" : "NV");
|
||||||
|
|
||||||
|
// Upsert: 1 row mỗi (PEId, phase, dept, stage). UNIQUE index enforce.
|
||||||
|
var existing = await db.PurchaseEvaluationDepartmentApprovals
|
||||||
|
.FirstOrDefaultAsync(a =>
|
||||||
|
a.PurchaseEvaluationId == evaluation.Id
|
||||||
|
&& a.PhaseAtApproval == (int)fromPhase
|
||||||
|
&& a.DepartmentId == deptId
|
||||||
|
&& a.Stage == stage, ct);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
db.PurchaseEvaluationDepartmentApprovals.Add(new PurchaseEvaluationDepartmentApproval
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = evaluation.Id,
|
||||||
|
PhaseAtApproval = (int)fromPhase,
|
||||||
|
DepartmentId = deptId,
|
||||||
|
Stage = stage,
|
||||||
|
ApproverUserId = actorUid,
|
||||||
|
ApproverRoleSnapshot = roleSnapshot,
|
||||||
|
Comment = comment,
|
||||||
|
ApprovedAt = dateTime.UtcNow,
|
||||||
|
IsBypassed = isBypassed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.ApproverUserId = actorUid;
|
||||||
|
existing.ApproverRoleSnapshot = roleSnapshot;
|
||||||
|
existing.Comment = comment;
|
||||||
|
existing.ApprovedAt = dateTime.UtcNow;
|
||||||
|
existing.IsBypassed = isBypassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Stage=Confirm tồn tại cho (PEId, fromPhase, deptId)
|
||||||
|
var hasConfirm = stage == ApprovalStage.Confirm
|
||||||
|
|| await db.PurchaseEvaluationDepartmentApprovals.AnyAsync(a =>
|
||||||
|
a.PurchaseEvaluationId == evaluation.Id
|
||||||
|
&& a.PhaseAtApproval == (int)fromPhase
|
||||||
|
&& a.DepartmentId == deptId
|
||||||
|
&& a.Stage == ApprovalStage.Confirm, ct);
|
||||||
|
|
||||||
|
if (!hasConfirm)
|
||||||
|
{
|
||||||
|
// NV review xong, chưa có TPB confirm → BLOCK phase transition.
|
||||||
|
// Log Approval + Changelog "đã review" để audit. Phase giữ nguyên.
|
||||||
|
db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = evaluation.Id,
|
||||||
|
FromPhase = fromPhase,
|
||||||
|
ToPhase = fromPhase, // không đổi phase
|
||||||
|
ApproverUserId = actorUid,
|
||||||
|
Decision = ApprovalDecision.Approve,
|
||||||
|
Comment = $"[Review NV] {comment ?? ""}",
|
||||||
|
ApprovedAt = dateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
string? reviewerName = (actor.FullName ?? actor.Email);
|
||||||
|
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = evaluation.Id,
|
||||||
|
EntityType = PurchaseEvaluationEntityType.Workflow,
|
||||||
|
Action = ChangelogAction.Transition,
|
||||||
|
PhaseAtChange = fromPhase,
|
||||||
|
UserId = actorUid,
|
||||||
|
UserName = reviewerName ?? "Hệ thống",
|
||||||
|
Summary = $"{reviewerName} (NV) đã review phase {fromPhase}, chờ TPB confirm",
|
||||||
|
ContextNote = comment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO Chunk E: notify TPB cùng dept để confirm.
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
evaluation.SlaWarningSent = false;
|
evaluation.SlaWarningSent = false;
|
||||||
evaluation.Phase = targetPhase;
|
evaluation.Phase = targetPhase;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user