From 364aef63fdb4e34c4a7698fb81cdc65e831b06e9 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 14 May 2026 23:08:11 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PurchaseEvaluation:=20Chunk=20B=20?= =?UTF-8?q?=E2=80=94=20Mig=2031=20K2=20Approver=20F2=20branch=20APPROVE=20?= =?UTF-8?q?STEP=20+=20DTO=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service ApproveV2Async +skipToFinal 8th param. APPROVE STEP branch sau UPSERT PEL opinion: check admin OR matchingLevel.AllowApproverSkipToFinal → set Phase=DaDuyet terminal directly, clear pointer + SLA, audit "[Approver duyệt thẳng Cấp cuối — Bước X Cấp Y → DaDuyet]". Non-admin + flag off → ConflictException. ApproveV1LegacyAsync: throw nếu skipToFinal=true non-admin (V1 legacy không hỗ trợ per-Approver-slot flag). Caller TransitionAsync line ~144 pass skipToFinal vào ApproveV2Async. Drafter SUBMIT branch ignore skipToFinal (K1 đã remove F2 Drafter semantic stub) — Mig 31 marker comment cleanup. DTO ApprovalWorkflowOptionsDto +bool AllowApproverSkipToFinal (7th field). DTO PurchaseEvaluationDetailBundleDto -DrafterAllowSkipToFinal field. GetPe handler populate 7 Allow* từ curLevel (Mig 29+30+31 cumulative). Sentinel `var drafterAllowSkipToFinal = false;` cleanup từ K1. IPurchaseEvaluationWorkflowService.cs comment skipToFinal semantic refactor: Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối. Pattern reusable: feedback_per_nv_permission_scope.md reinforced 3× cumulative (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2). Verify: - dotnet build production projects clean (0 err, 2 warnings pre-existing DocxRenderer) - Test fail at K1 expected (test file references removed prop, K7 sẽ fix) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Dtos/PurchaseEvaluationDtos.cs | 19 +++---- .../PurchaseEvaluationFeatures.cs | 15 ++---- .../IPurchaseEvaluationWorkflowService.cs | 10 ++-- .../PurchaseEvaluationWorkflowService.cs | 53 ++++++++++++++++--- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs index 2ef3d62..dc16540 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs @@ -81,15 +81,18 @@ public record PurchaseEvaluationChangelogDto( // Mig 29 (S21 t5) — Approver options của slot Level hiện tại (per-NV). // FE eOffice filter Trả lại dropdown + Edit Section 2 enabled theo flag của // Cấp hiện tại NV đang duyệt. Null nếu phiếu V1 legacy hoặc không ChoDuyet. -// F2 (Drafter skip) đã move sang `PeDetailBundleDto.DrafterAllowSkipToFinal`. // Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách". +// Mig 31 (S23 t1) — F2 refactor sang Approver scope ChoDuyet: +AllowApproverSkipToFinal +// cho phép Approver duyệt thẳng Cấp cuối (admin opt-in per slot). Storage cũ +// Users.AllowDrafterSkipToFinal đã drop, semantic Drafter-from-Nháp deprecated. public record ApprovalWorkflowOptionsDto( bool AllowReturnOneLevel, bool AllowReturnOneStep, bool AllowReturnToAssignee, bool AllowReturnToDrafter, bool AllowApproverEditDetails, - bool AllowApproverEditBudget); + bool AllowApproverEditBudget, + bool AllowApproverSkipToFinal); public record PurchaseEvaluationWorkflowSummaryDto( string PolicyName, @@ -207,14 +210,12 @@ public record PurchaseEvaluationDetailBundleDto( string? ApprovalWorkflowCode, string? ApprovalWorkflowName, int? ApprovalWorkflowVersion, - // Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null - // nếu V1 legacy hoặc không ChoDuyet. FE render Trả lại dropdown + Edit - // Section 2 conditional. Field rename "WorkflowOptions" → "CurrentLevelOptions" - // để rõ semantic per-slot không phải workflow-wide. + // Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options + // của Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc không ChoDuyet. + // FE render Trả lại dropdown + Edit Section 2 + Section 4 ngân sách + + // Duyệt thẳng Cấp cuối conditional. Field rename "WorkflowOptions" → + // "CurrentLevelOptions" để rõ semantic per-slot không phải workflow-wide. ApprovalWorkflowOptionsDto? CurrentLevelOptions, - // Mig 29 — F2 per-Drafter: cờ AllowDrafterSkipToFinal của Drafter user pin - // phiếu. Workspace conditional render checkbox "Gửi thẳng Cấp cuối". - bool DrafterAllowSkipToFinal, PurchaseEvaluationCurrentApprovalDto? CurrentApproval, PurchaseEvaluationApprovalFlowDto? ApprovalFlow, List Suppliers, diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs index 6b44379..9236401 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs @@ -728,11 +728,6 @@ public class GetPurchaseEvaluationQueryHandler( // hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)"). // Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full // Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards. - // Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels.AllowApproverSkipToFinal - // (per-Approver slot). Drafter flag retired. DTO field kept transiently (K2 sẽ refactor - // FE Workspace remove checkbox "Gửi thẳng Cấp cuối" Drafter mode + Approve panel hiện - // checkbox dynamic theo Level.AllowApproverSkipToFinal). Sentinel false. - var drafterAllowSkipToFinal = false; string? awCode = null, awName = null; int? awVersion = null; @@ -751,8 +746,9 @@ public class GetPurchaseEvaluationQueryHandler( awName = aw.Name; awVersion = aw.Version; - // Mig 29 (S21 t5) — Resolve Cấp hiện tại + populate 5 Allow* flag - // của slot Approver đang duyệt. Null nếu pointer chưa init. + // Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — Resolve + // Cấp hiện tại + populate 7 Allow* flag của slot Approver đang + // duyệt. Null nếu pointer chưa init. if (e.CurrentWorkflowStepIndex is int curStepIdx && curStepIdx >= 0 && curStepIdx < aw.Steps.Count && e.CurrentApprovalLevelOrder is int curLevelOrder) @@ -767,7 +763,8 @@ public class GetPurchaseEvaluationQueryHandler( curLevel.AllowReturnToAssignee, curLevel.AllowReturnToDrafter, curLevel.AllowApproverEditDetails, - curLevel.AllowApproverEditBudget); + curLevel.AllowApproverEditBudget, + curLevel.AllowApproverSkipToFinal); } } @@ -890,8 +887,6 @@ public class GetPurchaseEvaluationQueryHandler( e.BudgetId, budgetSummary, e.BudgetManualName, e.BudgetManualAmount, e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions, - // Mig 29 (S21 t5) — F2 drafter flag từ User entity - drafterAllowSkipToFinal, currentApproval, approvalFlow, e.Suppliers .OrderBy(s => s.Order) diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs index 4590f8b..eb33d98 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs @@ -8,13 +8,15 @@ public interface IPurchaseEvaluationWorkflowService // Kiểm tra + thực hiện transition. Throw ForbiddenException nếu không hợp lệ. // Tự tạo PurchaseEvaluationApproval + update Phase + SlaDeadline. // - // Optional params Mig 28 (S21 t4 — F1+F2 advanced workflow options): + // Optional params Mig 28-31 (S21-S23 advanced workflow options per-NV slot): // - returnMode: mode Trả lại (F1). Null = default Drafter behavior khi Reject+TraLai. // OneLevel/OneStep/Assignee → giữ Phase=ChoDuyet, lùi pointer (peer review). - // Drafter → Phase=TraLai clear pointer như S17. + // Drafter → Phase=TraLai clear pointer như S17. Flag check tại level.Allow*. // - returnTargetUserId: required khi returnMode=Assignee — pick từ list NV đã duyệt. - // - skipToFinal: F2 Drafter trình duyệt → skip mọi Bước/Cấp trung gian, set pointer - // = max Step + max Level. Workflow phải AllowDrafterSkipToFinal=true. + // - skipToFinal: F2 Approver during ChoDuyet duyệt thẳng Cấp cuối → set Phase=DaDuyet + // terminal trực tiếp, clear pointer. Mig 31 (S23 t1) refactor sang Approver scope: + // matchingLevel.AllowApproverSkipToFinal phải true (admin opt-in per slot). + // Semantic cũ Drafter-from-Nháp đã deprecated, storage Users.AllowDrafterSkipToFinal dropped. Task TransitionAsync( PurchaseEvaluation evaluation, PurchaseEvaluationPhase targetPhase, diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index 78fe7a0..e642f81 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -118,13 +118,10 @@ public class PurchaseEvaluationWorkflowService( } evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet; - // Mig 31 (S23 t1 Plan K Chunk A) — F2 Drafter SUBMIT skipToFinal branch - // REMOVED stub. Semantic refactor: F2 cũ Drafter-skip-from-Nháp → mới - // Approver-skip-during-ChoDuyet (storage move sang - // ApprovalWorkflowLevels.AllowApproverSkipToFinal per-slot Approver). - // TransitionAsync `bool skipToFinal` 8th param KEPT cho K2 sẽ repurpose - // to APPROVE STEP branch (line ~393-525 V2 path). - // K2 sẽ add Approver F2 branch trong APPROVE STEP (line ~393-525) + // Mig 31 (S23 t1 Plan K) — F2 Drafter-skip-from-Nháp semantic deprecated. + // skipToFinal param 8th repurpose sang Approver scope ChoDuyet (xem + // ApproveV2Async branch). Drafter SUBMIT chạy normal init pointer Step 0 + // Cấp 1, ignore skipToFinal flag. evaluation.CurrentWorkflowStepIndex = 0; // Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set). evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null; @@ -139,12 +136,17 @@ public class PurchaseEvaluationWorkflowService( { // Branch: V2 schema mới (ApprovalWorkflowId pin) hay V1 legacy // (WorkflowDefinitionId pin Mig 21). + // Mig 31 (S23 t1 Plan K) — skipToFinal repurpose Approver scope ChoDuyet. + // V2 path nhận flag, V1 legacy throw nếu non-admin gọi skipToFinal=true. if (evaluation.ApprovalWorkflowId is Guid awId) { - await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, ct); + await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, skipToFinal, ct); } else { + if (skipToFinal && !isAdmin && !isSystem) + throw new ConflictException( + "skipToFinal chỉ hỗ trợ phiếu V2 (ApprovalWorkflowsV2). Phiếu V1 legacy không có per-Approver-slot flag."); await ApproveV1LegacyAsync(evaluation, actorUserId, actorRoles, isAdmin, isSystem, comment, ct); } await db.SaveChangesAsync(ct); @@ -364,6 +366,8 @@ public class PurchaseEvaluationWorkflowService( } // ===== V2 schema (Mig 22-24) — iterate ApprovalWorkflowSteps + Levels ===== + // Mig 31 (S23 t1 Plan K) — `skipToFinal` 8th param: F2 Approver scope ChoDuyet. + // Admin opt-in flag per slot tại matchingLevel.AllowApproverSkipToFinal. private async Task ApproveV2Async( PurchaseEvaluation evaluation, Guid awId, @@ -372,6 +376,7 @@ public class PurchaseEvaluationWorkflowService( bool isAdmin, bool isSystem, string? comment, + bool skipToFinal, CancellationToken ct) { var aw = await db.ApprovalWorkflows.AsNoTracking() @@ -462,6 +467,38 @@ public class PurchaseEvaluationWorkflowService( existingOpinion.SignedByFullName = actorFullName; } + // Mig 31 (S23 t1 Plan K) — F2 Approver scope ChoDuyet: duyệt thẳng Cấp cuối. + // Admin opt-in per slot tại matchingLevel.AllowApproverSkipToFinal. Khi + // Approver tick checkbox "Duyệt thẳng Cấp cuối" trong Workspace + admin + // đã enable flag cho slot này → bỏ qua mọi Bước/Cấp trung gian còn lại, + // set Phase=DaDuyet terminal trực tiếp. Mirror F3+F4 admin opt-in per- + // Approver-slot pattern (Mig 29 + Mig 30) reinforced 3× cumulative. + // Non-admin + flag off → ConflictException. Admin bypass flag. + if (skipToFinal) + { + if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal) + { + throw new ConflictException( + $"Cấp Approver hiện tại (Bước {currentIdx + 1} Cấp {currentLevelOrder}) " + + "chưa được phép duyệt thẳng Cấp cuối. Admin phải tick checkbox " + + "'Duyệt thẳng Cấp cuối' trong Workflow Designer cho slot này."); + } + + evaluation.Phase = PurchaseEvaluationPhase.DaDuyet; + evaluation.CurrentWorkflowStepIndex = null; + evaluation.CurrentApprovalLevelOrder = null; + evaluation.SlaDeadline = null; + await LogTransitionAsync( + evaluation, + PurchaseEvaluationPhase.ChoDuyet, + PurchaseEvaluationPhase.DaDuyet, + actorUserId, + ApprovalDecision.Approve, + $"[Approver duyệt thẳng Cấp cuối — Bước {currentIdx + 1} Cấp {currentLevelOrder} → DaDuyet] {comment ?? ""}".Trim(), + ct); + return; + } + // Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1 if (currentLevelOrder < maxLevelOrder) {