[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? ApproverUserName,
string? ApproverEmail, string? ApproverEmail,
// Mig 29 (S21 t5) — 5 advanced options per slot Approver (F1 mode Trả lại // 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 AllowReturnOneLevel,
bool AllowReturnOneStep, bool AllowReturnOneStep,
bool AllowReturnToAssignee, bool AllowReturnToAssignee,
bool AllowReturnToDrafter, bool AllowReturnToDrafter,
bool AllowApproverEditDetails); bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record AwStepDto( public record AwStepDto(
Guid Id, Guid Id,
@ -152,7 +154,7 @@ public class GetAwAdminOverviewQueryHandler(
// Mig 29 (S21 t5) — 5 Allow* flag per slot Level // 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, return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email,
l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee, l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee,
l.AllowReturnToDrafter, l.AllowApproverEditDetails); l.AllowReturnToDrafter, l.AllowApproverEditDetails, l.AllowApproverEditBudget);
}).ToList() }).ToList()
)).ToList()); )).ToList());
@ -184,12 +186,13 @@ public record CreateAwLevelInput(
Guid ApproverUserId, Guid ApproverUserId,
// Mig 29 (S21 t5) — 5 Allow* options per slot. Admin Designer tick per // 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 // 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 AllowReturnOneLevel = false,
bool AllowReturnOneStep = false, bool AllowReturnOneStep = false,
bool AllowReturnToAssignee = false, bool AllowReturnToAssignee = false,
bool AllowReturnToDrafter = true, bool AllowReturnToDrafter = true,
bool AllowApproverEditDetails = false); bool AllowApproverEditDetails = false,
bool AllowApproverEditBudget = false);
public record CreateAwStepInput( public record CreateAwStepInput(
int Order, int Order,
@ -315,6 +318,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
AllowReturnToAssignee = l.AllowReturnToAssignee, AllowReturnToAssignee = l.AllowReturnToAssignee,
AllowReturnToDrafter = l.AllowReturnToDrafter, AllowReturnToDrafter = l.AllowReturnToDrafter,
AllowApproverEditDetails = l.AllowApproverEditDetails, AllowApproverEditDetails = l.AllowApproverEditDetails,
AllowApproverEditBudget = l.AllowApproverEditBudget,
}).ToList(), }).ToList(),
}) })
.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 // 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. // 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`. // 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( public record ApprovalWorkflowOptionsDto(
bool AllowReturnOneLevel, bool AllowReturnOneLevel,
bool AllowReturnOneStep, bool AllowReturnOneStep,
bool AllowReturnToAssignee, bool AllowReturnToAssignee,
bool AllowReturnToDrafter, bool AllowReturnToDrafter,
bool AllowApproverEditDetails); bool AllowApproverEditDetails,
bool AllowApproverEditBudget);
public record PurchaseEvaluationWorkflowSummaryDto( public record PurchaseEvaluationWorkflowSummaryDto(
string PolicyName, string PolicyName,

View File

@ -290,7 +290,9 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler(
} }
else if (entity.Phase == PurchaseEvaluationPhase.ChoDuyet) 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) 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."); 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) 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."); throw new ConflictException("Pointer step out of range — schema lỗi.");
var step = stepsOrdered[csi]; var step = stepsOrdered[csi];
var level = step.Levels.FirstOrDefault(l => l.Order == curLvl); 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( 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."); $"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}]"; actorTag = $"[Approver Bước {step.Order}/Cấp {curLvl}]";
@ -761,7 +770,8 @@ public class GetPurchaseEvaluationQueryHandler(
curLevel.AllowReturnOneStep, curLevel.AllowReturnOneStep,
curLevel.AllowReturnToAssignee, curLevel.AllowReturnToAssignee,
curLevel.AllowReturnToDrafter, curLevel.AllowReturnToDrafter,
curLevel.AllowApproverEditDetails); curLevel.AllowApproverEditDetails,
curLevel.AllowApproverEditBudget);
} }
} }

View File

@ -98,5 +98,11 @@ public class ApprovalWorkflowLevel : BaseEntity
/// duyệt. KHÔNG đụng PE Header, KHÔNG reset workflow. /// duyệt. KHÔNG đụng PE Header, KHÔNG reset workflow.
public bool AllowApproverEditDetails { get; set; } public bool AllowApproverEditDetails { get; set; }
/// F4 (Mig 30 — S22+5) — Cho phép NV này edit Section "Điều chỉnh ngân sách"
/// (BudgetId / BudgetManualName / BudgetManualAmount) lúc đang duyệt. Default
/// false (admin opt-in per slot). Logic edit ngân sách giữ nguyên Drafter
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
public bool AllowApproverEditBudget { get; set; }
public ApprovalWorkflowStep? Step { get; set; } public ApprovalWorkflowStep? Step { get; set; }
} }

View File

@ -77,5 +77,9 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
e.Property(x => x.AllowReturnToAssignee).HasDefaultValue(false); e.Property(x => x.AllowReturnToAssignee).HasDefaultValue(false);
e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true); e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true);
e.Property(x => x.AllowApproverEditDetails).HasDefaultValue(false); e.Property(x => x.AllowApproverEditDetails).HasDefaultValue(false);
// Mig 30 (S22+5) — F4 per-NV: cho phép edit Section "Điều chỉnh ngân sách"
// lúc đang duyệt. Default false (admin opt-in).
e.Property(x => x.AllowApproverEditBudget).HasDefaultValue(false);
} }
} }

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAllowApproverEditBudgetToLevels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AllowApproverEditBudget",
table: "ApprovalWorkflowLevels",
type: "bit",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AllowApproverEditBudget",
table: "ApprovalWorkflowLevels");
}
}
}

View File

@ -188,6 +188,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<bool>("AllowApproverEditBudget")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("AllowApproverEditDetails") b.Property<bool>("AllowApproverEditDetails")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")