[CLAUDE] PE-Workflow: S22+5 Chunk A — Mig 30 +AllowApproverEditBudget per-Level slot

Bro clarify spec S22+4:
- KHÔNG đổi logic edit ngân sách (Drafter Nháp/TraLai vẫn duy nhất default)
- Thêm flag per-NV slot trong Designer: "Cho phép NV này edit Section ngân sách
  lúc đang duyệt" (mirror pattern F3 AllowApproverEditDetails Mig 29)

Mig 30 `AddAllowApproverEditBudgetToLevels`:
- ALTER ApprovalWorkflowLevels +AllowApproverEditBudget bit NOT NULL DEFAULT 0
- 3-file rule (mig.cs + Designer.cs + Snapshot.cs)
- Apply LocalDB Dev + Design

Domain entity ApprovalWorkflowLevel +AllowApproverEditBudget (default false).
EF config HasDefaultValue(false). DTO AwLevelDto + ApprovalWorkflowOptionsDto
+ CreateAwLevelInput all extend +AllowApproverEditBudget.

PE GET handler populate currentLevelOptions thêm AllowApproverEditBudget từ
curLevel slot. Admin Designer GET/POST handler propagate flag.

AdjustBudgetCommand handler refactor ChoDuyet branch:
- Trước: check actor match level.ApproverUserId (cho phép mặc định)
- Sau: check level.AllowApproverEditBudget=true AND actor match ApproverUserId
  → throw ConflictException nếu slot chưa được cấp quyền

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Mig 30 applied Dev + Design DB

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 23:09:48 +07:00
parent 30d51c89bb
commit b079b27343
8 changed files with 4005 additions and 9 deletions

View File

@ -28,12 +28,14 @@ public record AwLevelDto(
string? ApproverUserName,
string? ApproverEmail,
// Mig 29 (S21 t5) — 5 advanced options per slot Approver (F1 mode Trả lại
// + F3 Edit Section 2). Mỗi NV trong workflow có quyền riêng.
// + F3 Edit Section 2). Mig 30 (S22+5) — F4 +AllowApproverEditBudget.
// Mỗi NV trong workflow có quyền riêng.
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowApproverEditDetails);
bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record AwStepDto(
Guid Id,
@ -152,7 +154,7 @@ public class GetAwAdminOverviewQueryHandler(
// Mig 29 (S21 t5) — 5 Allow* flag per slot Level
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email,
l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee,
l.AllowReturnToDrafter, l.AllowApproverEditDetails);
l.AllowReturnToDrafter, l.AllowApproverEditDetails, l.AllowApproverEditBudget);
}).ToList()
)).ToList());
@ -184,12 +186,13 @@ public record CreateAwLevelInput(
Guid ApproverUserId,
// Mig 29 (S21 t5) — 5 Allow* options per slot. Admin Designer tick per
// Level row. Default backward compat: AllowReturnToDrafter=true, 4 còn lại
// false (admin opt-in từng slot).
// false (admin opt-in từng slot). Mig 30 (S22+5) — F4 AllowApproverEditBudget.
bool AllowReturnOneLevel = false,
bool AllowReturnOneStep = false,
bool AllowReturnToAssignee = false,
bool AllowReturnToDrafter = true,
bool AllowApproverEditDetails = false);
bool AllowApproverEditDetails = false,
bool AllowApproverEditBudget = false);
public record CreateAwStepInput(
int Order,
@ -315,6 +318,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
AllowReturnToAssignee = l.AllowReturnToAssignee,
AllowReturnToDrafter = l.AllowReturnToDrafter,
AllowApproverEditDetails = l.AllowApproverEditDetails,
AllowApproverEditBudget = l.AllowApproverEditBudget,
}).ToList(),
})
.ToList(),

View File

@ -82,12 +82,14 @@ public record PurchaseEvaluationChangelogDto(
// 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".
public record ApprovalWorkflowOptionsDto(
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowApproverEditDetails);
bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record PurchaseEvaluationWorkflowSummaryDto(
string PolicyName,

View File

@ -290,7 +290,9 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler(
}
else if (entity.Phase == PurchaseEvaluationPhase.ChoDuyet)
{
// Approver scope — actor phải là ApproverUserId của currentLevel
// F4 (Mig 30 — S22+5) — Approver scope chỉ accept khi
// currentLevel.AllowApproverEditBudget=true (admin Designer tick
// per slot) AND actor match ApproverUserId.
if (entity.ApprovalWorkflowId is not Guid awId)
throw new ConflictException("Phiếu V1 legacy không hỗ trợ điều chỉnh ngân sách lúc đang duyệt.");
if (entity.CurrentWorkflowStepIndex is not int csi || entity.CurrentApprovalLevelOrder is not int curLvl)
@ -307,7 +309,14 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler(
throw new ConflictException("Pointer step out of range — schema lỗi.");
var step = stepsOrdered[csi];
var level = step.Levels.FirstOrDefault(l => l.Order == curLvl);
if (level?.ApproverUserId != actorId)
if (level is null)
throw new ConflictException("Cấp duyệt không tìm thấy — schema lỗi.");
if (!level.AllowApproverEditBudget)
throw new ConflictException(
$"Cấp Approver hiện tại (Bước {step.Order} / Cấp {curLvl}) " +
"không được cấp quyền chỉnh sửa Section ngân sách. " +
"Liên hệ Admin Designer cấp quyền slot.");
if (level.ApproverUserId != actorId)
throw new ForbiddenException(
$"Chỉ NV phụ trách Bước {step.Order} / Cấp {curLvl} mới được điều chỉnh ngân sách lúc đang duyệt.");
actorTag = $"[Approver Bước {step.Order}/Cấp {curLvl}]";
@ -761,7 +770,8 @@ public class GetPurchaseEvaluationQueryHandler(
curLevel.AllowReturnOneStep,
curLevel.AllowReturnToAssignee,
curLevel.AllowReturnToDrafter,
curLevel.AllowApproverEditDetails);
curLevel.AllowApproverEditDetails,
curLevel.AllowApproverEditBudget);
}
}