[CLAUDE] Domain: Chunk A — Mig 31 swap F2 storage Users→ApprovalWorkflowLevels (Approver scope ChoDuyet)

Mig 31 RefactorSkipToFinalToApproverLevel — 2 stage manual reorder:
- ADD ApprovalWorkflowLevels.AllowApproverSkipToFinal bit NOT NULL DEFAULT 0
- DROP Users.AllowDrafterSkipToFinal (semantic mới khác hẳn — admin re-config qua Designer)
- NO BACKFILL (Option A — accept lose 4 prod user value per K0-bis audit)

Plan K refactor F2 semantic: Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối.
Mirror F3+F4 admin opt-in per-Approver-slot pattern (Mig 29 + Mig 30) reinforced 3× cumulative.

Service line 121-157 F2 Drafter SUBMIT branch REMOVED stub (K2 sẽ add Approver F2 branch
trong APPROVE STEP line ~393-525). TransitionAsync skipToFinal param 8th KEPT cho K2 repurpose.

Application layer compile-break fix transient: UserDto field mapping + GET handler + LIST
handler + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures
drafter flag → sentinel false. DTO + Command signature UNCHANGED (K2 chunk Chủ trì sẽ
refactor DTO/Command theo plan).

4 prod user (fin.pp + pm.nv + nv.test + truong.nguyen) lose AllowDrafterSkipToFinal=true
per bro Option A. Audit trail trong session log K8.

Verify:
- dotnet ef migrations add pass
- dotnet ef database update Dev + Design pass (Mig 31 applied both DB)
- dotnet build src/Backend/SolutionErp.Api production projects clean (0 err, 0 warn)
- dotnet test SKIPPED per UAT mode (memory feedback_uat_skip_verify) — K7 chunk fix
  remaining PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253 reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-14 23:03:05 +07:00
parent 56868bfd7f
commit db6625304a
10 changed files with 4039 additions and 63 deletions

View File

@ -81,5 +81,10 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
// 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);
// Mig 31 (S23 t1 Plan K Chunk A) — F2 per-NV REFACTOR: cho phép Approver
// slot này skip thẳng Cấp cuối lúc đang duyệt ChoDuyet. Default false
// (admin opt-in). Semantic + storage move từ Users.AllowDrafterSkipToFinal.
e.Property(x => x.AllowApproverSkipToFinal).HasDefaultValue(false);
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
// Mig 31 (S23 t1 Plan K Chunk A) — F2 storage swap Users → ApprovalWorkflowLevels.
// Manual reorder Up(): ADD col first → DROP col second (per memory rule
// feedback_ef_migration_backfill_reorder.md). NO BACKFILL block — Option A bro
// accept 4 prod user (fin.pp + pm.nv + nv.test + truong.nguyen) lose value cũ
// AllowDrafterSkipToFinal=true. Admin re-config qua Workflow Designer per slot.
//
// Down() reverse order với data loss accepted: cờ AllowApproverSkipToFinal sẽ
// mất khi rollback (Users.AllowDrafterSkipToFinal restore default false).
public partial class RefactorSkipToFinalToApproverLevel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Step 1 — ADD new column on ApprovalWorkflowLevels (per-slot Approver scope).
migrationBuilder.AddColumn<bool>(
name: "AllowApproverSkipToFinal",
table: "ApprovalWorkflowLevels",
type: "bit",
nullable: false,
defaultValue: false);
// Step 2 — DROP old column on Users (NO BACKFILL — Option A accept lose).
migrationBuilder.DropColumn(
name: "AllowDrafterSkipToFinal",
table: "Users");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Step 1 — DROP new column on ApprovalWorkflowLevels.
migrationBuilder.DropColumn(
name: "AllowApproverSkipToFinal",
table: "ApprovalWorkflowLevels");
// Step 2 — ADD old column on Users (data loss accepted — default false).
migrationBuilder.AddColumn<bool>(
name: "AllowDrafterSkipToFinal",
table: "Users",
type: "bit",
nullable: false,
defaultValue: false);
}
}
}

View File

@ -198,6 +198,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("AllowApproverSkipToFinal")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("AllowReturnOneLevel")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
@ -1945,9 +1950,6 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<bool>("AllowDrafterSkipToFinal")
.HasColumnType("bit");
b.Property<bool>("CanBypassReview")
.HasColumnType("bit");

View File

@ -118,43 +118,16 @@ public class PurchaseEvaluationWorkflowService(
}
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
// 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.");
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
?? throw new ConflictException($"Bước {finalStep.Order} chưa có Cấp nào.");
evaluation.CurrentWorkflowStepIndex = wfSkip.Steps.Count - 1; // 0-based last step
evaluation.CurrentApprovalLevelOrder = finalLevelOrder;
comment = string.IsNullOrWhiteSpace(comment)
? "[Drafter gửi thẳng Cấp cuối — skip Bước/Cấp trung gian]"
: $"{comment} [Drafter gửi thẳng Cấp cuối — skip Bước/Cấp trung gian]";
}
else
{
evaluation.CurrentWorkflowStepIndex = 0;
// Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null;
}
// 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)
evaluation.CurrentWorkflowStepIndex = 0;
// Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null;
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
await LogTransitionAsync(evaluation, fromPhase, PurchaseEvaluationPhase.ChoDuyet, actorUserId, decision, comment, ct);
await db.SaveChangesAsync(ct);