[CLAUDE] PE-Workflow: S21 t5 Chunk A — Mig 29 refactor Allow* sang per-NV (per-Level + per-Drafter)
Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4) sang per-NV scope: - F1 (4 mode Trả lại) + F3 (Edit Section 2) → 5 flag MOVE xuống `ApprovalWorkflowLevels` (per slot Approver, cùng table với ApproverUserId). - F2 (AllowDrafterSkipToFinal) → MOVE xuống `Users` (per-Drafter user, User Mgmt). Mig 29 `RefactorAdvancedOptionsToPerLevelAndDrafterUser` — 4-stage migration (EF auto-generated drop-then-add đã được REORDER manual): 1. ADD 5 column trên `ApprovalWorkflowLevels` (AllowReturnOneLevel/OneStep/ ToAssignee/ToDrafter[default true]/AllowApproverEditDetails) 2. ADD 1 column trên `Users` (AllowDrafterSkipToFinal default false) 3. BACKFILL bulk SQL (preserve admin config Mig 28): - Levels: copy workflow.Allow* → all Levels của workflow (JOIN Steps) - Users: SET TRUE cho user nào từng Drafter PE link workflow Allow=true 4. DROP 6 column workflow-level (Mig 28 cleanup) 3-file rule complete. Apply LocalDB Dev + Design success. Domain entity refactor: - `ApprovalWorkflow.cs` — REMOVE 6 Allow* field (S21 t4 Mig 28 cũ) - `ApprovalWorkflowLevel.cs` — ADD 5 Allow* field (F1 + F3) - `User.cs` — ADD 1 Allow* field (F2 AllowDrafterSkipToFinal) EF config update: - `ApprovalWorkflowConfiguration.cs` — remove 6 HasDefaultValue workflow-level, add 5 HasDefaultValue per-Level (4 false + 1 AllowReturnToDrafter true S17) Service refactor `ApplyReturnModeAsync` (`PurchaseEvaluationWorkflowService.cs`): - Resolve currentLevel slot (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder) - Read 5 Allow* từ `currentLevel.AllowXxx` thay vì workflow.Allow* - Admin bypass per-Level flag check (unchanged behavior) - Drafter mode đặc biệt: check AllowReturnToDrafter của currentLevel (vẫn validate) - V1 legacy (no V2 schema) → fallback Drafter behavior tự động DRAFTER trình refactor (`TransitionAsync` skipToFinal branch): - Permission check moved from workflow-level → `drafterUser.AllowDrafterSkipToFinal` - Use `userManager.FindByIdAsync(actorUserId)` để get current Drafter user entity - Admin bypass user flag check (unchanged) Helper `EnsureEditableForDetailsAsync` refactor: - Read `level.AllowApproverEditDetails` thay vì workflow.AllowApproverEditDetails - Error message rõ "Cấp Approver hiện tại (Bước X / Cấp Y)" thay vì "Workflow" DTO refactor: - `AwLevelDto` ADD 5 Allow* field (admin Designer GET per-Level) - `AwDefinitionDto` REMOVE 6 Allow* (no longer workflow-level) - `CreateAwLevelInput` ADD 5 Allow* param (admin Designer POST per-Level) - `CreateAwDefinitionCommand` REMOVE 6 Allow* (Steps[].Levels[] now has them) - `ApprovalWorkflowOptionsDto` chỉ còn 5 flag (F2 removed — separate field) - `PurchaseEvaluationDetailBundleDto`: - rename `WorkflowOptions` → `CurrentLevelOptions` (clearer semantic per-slot) - ADD `DrafterAllowSkipToFinal bool` (resolve từ DrafterUserId → User entity) GetPurchaseEvaluationQueryHandler populate: - `currentLevelOptions` = 5 Allow* của Cấp hiện tại (null nếu V1 legacy / no pointer) - `drafterAllowSkipToFinal` = User.AllowDrafterSkipToFinal lookup từ DrafterUserId Backward compat verified: - Mig 29 backfill preserve admin config S21 t4 — workflow cũ vẫn chạy đúng sau deploy. User chưa từng làm Drafter F2 phải opt-in lần đầu (no auto-set). - 84 test PASS (58 Domain + 26 Infra unchanged, 3 gotcha #45 guard test backward compat signature). Pending Chunk B/C: FE Admin Designer move 5 checkbox xuống per-Level slot + FE eOffice read currentLevelOptions + drafterAllowSkipToFinal. 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.Notifications;
|
||||
using SolutionErp.Application.PurchaseEvaluations.Services;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
@ -111,19 +112,27 @@ public class PurchaseEvaluationWorkflowService(
|
||||
}
|
||||
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
||||
|
||||
// F2 (Mig 28 — S21 t4) — Drafter skip thẳng Cấp cuối. Workflow phải
|
||||
// AllowDrafterSkipToFinal=true. Set pointer = max Step + max Level.
|
||||
// Audit changelog ghi rõ "Drafter skip" để approver Cấp cuối biết.
|
||||
// F2 (Mig 29 — S21 t5) — Drafter skip thẳng Cấp cuối. Permission
|
||||
// check moved sang `User.AllowDrafterSkipToFinal` (per-Drafter user,
|
||||
// không còn workflow-level Mig 28).
|
||||
// Admin bypass user flag check.
|
||||
if (skipToFinal && evaluation.ApprovalWorkflowId is Guid skipAwId)
|
||||
{
|
||||
if (!isAdmin)
|
||||
{
|
||||
if (actorUserId is null)
|
||||
throw new ConflictException("skipToFinal yêu cầu authenticated user.");
|
||||
var drafterUser = await userManager.FindByIdAsync(actorUserId.Value.ToString())
|
||||
?? throw new ConflictException("User không tồn tại.");
|
||||
if (!drafterUser.AllowDrafterSkipToFinal)
|
||||
throw new ConflictException(
|
||||
$"User '{drafterUser.FullName}' không được phép gửi thẳng Cấp cuối. " +
|
||||
"Liên hệ Admin để cấp quyền ở User Management.");
|
||||
}
|
||||
var wfSkip = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == skipAwId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
if (!wfSkip.AllowDrafterSkipToFinal)
|
||||
throw new ConflictException(
|
||||
"Workflow không bật mode 'Gửi thẳng Cấp cuối'. " +
|
||||
"Liên hệ Admin để config Designer.");
|
||||
var finalStep = wfSkip.Steps.OrderBy(s => s.Order).LastOrDefault()
|
||||
?? throw new ConflictException("Workflow chưa có Bước nào.");
|
||||
var finalLevelOrder = finalStep.Levels.OrderBy(l => l.Order).LastOrDefault()?.Order
|
||||
@ -189,18 +198,57 @@ public class PurchaseEvaluationWorkflowService(
|
||||
bool isAdmin,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Mode Drafter — Session 17 default (always allowed for backward compat,
|
||||
// workflow.AllowReturnToDrafter default true).
|
||||
// Mig 29 (S21 t5) refactor: Allow* flag đã move xuống ApprovalWorkflowLevel
|
||||
// (per-slot Approver). Cần load workflow Steps+Levels để lấy Level hiện
|
||||
// tại (curStepIdx + curLevel).
|
||||
if (evaluation.ApprovalWorkflowId is not Guid awId)
|
||||
{
|
||||
// Phiếu V1 legacy không có Allow* → fallback Drafter (S17 behavior).
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
evaluation.SlaDeadline = null;
|
||||
return mode == WorkflowReturnMode.Drafter
|
||||
? "Trả về Người soạn thảo"
|
||||
: $"Trả về Người soạn thảo (fallback — phiếu V1 không hỗ trợ mode '{mode}')";
|
||||
}
|
||||
|
||||
var workflow = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
|
||||
var stepsOrdered = workflow.Steps.OrderBy(s => s.Order).ToList();
|
||||
|
||||
// Resolve Level hiện tại (slot Approver đang duyệt) — đọc Allow* từ slot
|
||||
// này. Required cho mọi mode (kể cả Drafter — Approver hiện tại quyết
|
||||
// định mode Trả lại theo flag riêng của slot).
|
||||
ApprovalWorkflowLevel? currentLevel = null;
|
||||
if (evaluation.CurrentWorkflowStepIndex is int csi && csi >= 0 && csi < stepsOrdered.Count)
|
||||
{
|
||||
var step = stepsOrdered[csi];
|
||||
currentLevel = step.Levels.FirstOrDefault(l => l.Order == evaluation.CurrentApprovalLevelOrder);
|
||||
}
|
||||
|
||||
// Validate Allow* flag từ Level slot hiện tại (Admin bypass)
|
||||
if (!isAdmin && currentLevel is not null)
|
||||
{
|
||||
var allowed = mode switch
|
||||
{
|
||||
WorkflowReturnMode.OneLevel => currentLevel.AllowReturnOneLevel,
|
||||
WorkflowReturnMode.OneStep => currentLevel.AllowReturnOneStep,
|
||||
WorkflowReturnMode.Assignee => currentLevel.AllowReturnToAssignee,
|
||||
WorkflowReturnMode.Drafter => currentLevel.AllowReturnToDrafter,
|
||||
_ => false,
|
||||
};
|
||||
if (!allowed)
|
||||
throw new ConflictException(
|
||||
$"Cấp Approver hiện tại không bật mode '{mode}'. Liên hệ Admin Designer để config Level slot.");
|
||||
}
|
||||
|
||||
// Mode Drafter — Session 17 default (Phase=TraLai clear pointer)
|
||||
if (mode == WorkflowReturnMode.Drafter)
|
||||
{
|
||||
// Validate workflow flag (admin có thể disable mode này force peer review)
|
||||
if (evaluation.ApprovalWorkflowId is Guid awId0 && !isAdmin)
|
||||
{
|
||||
var wf0 = await db.ApprovalWorkflows.FirstOrDefaultAsync(w => w.Id == awId0, ct);
|
||||
if (wf0 is not null && !wf0.AllowReturnToDrafter)
|
||||
throw new ConflictException(
|
||||
"Workflow không bật mode 'Trả về Drafter'. Phải dùng mode khác.");
|
||||
}
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
@ -208,38 +256,12 @@ public class PurchaseEvaluationWorkflowService(
|
||||
return "Trả về Người soạn thảo";
|
||||
}
|
||||
|
||||
// 3 mode còn lại (OneLevel / OneStep / Assignee) — yêu cầu V2 schema +
|
||||
// pointer hợp lệ.
|
||||
if (evaluation.ApprovalWorkflowId is not Guid awId)
|
||||
throw new ConflictException(
|
||||
$"Mode '{mode}' yêu cầu phiếu pin V2 workflow (ApprovalWorkflowId).");
|
||||
// 3 mode còn lại — yêu cầu pointer hợp lệ
|
||||
if (evaluation.CurrentWorkflowStepIndex is not int curStepIdx
|
||||
|| evaluation.CurrentApprovalLevelOrder is not int curLevel)
|
||||
throw new ConflictException(
|
||||
$"Mode '{mode}' yêu cầu phiếu đang ChoDuyet + pointer init. " +
|
||||
$"State hiện tại: Step={evaluation.CurrentWorkflowStepIndex}, Level={evaluation.CurrentApprovalLevelOrder}.");
|
||||
|
||||
var workflow = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
|
||||
// Validate Allow* flag (Admin bypass — admin có thể trả lại bất chấp config)
|
||||
if (!isAdmin)
|
||||
{
|
||||
var allowed = mode switch
|
||||
{
|
||||
WorkflowReturnMode.OneLevel => workflow.AllowReturnOneLevel,
|
||||
WorkflowReturnMode.OneStep => workflow.AllowReturnOneStep,
|
||||
WorkflowReturnMode.Assignee => workflow.AllowReturnToAssignee,
|
||||
_ => false,
|
||||
};
|
||||
if (!allowed)
|
||||
throw new ConflictException(
|
||||
$"Workflow không bật mode '{mode}'. Liên hệ Admin Designer để config.");
|
||||
}
|
||||
|
||||
var stepsOrdered = workflow.Steps.OrderBy(s => s.Order).ToList();
|
||||
var summary = string.Empty;
|
||||
|
||||
switch (mode)
|
||||
|
||||
Reference in New Issue
Block a user