[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:
@ -204,6 +204,7 @@ KHÔNG `*` / `latest`. Critical pins:
|
||||
|
||||
## 📅 Recent activity (last 10 FIFO)
|
||||
|
||||
- **2026-05-14 (S23 t1, K1 Chunk A PASS):** Mig 31 schema swap F2 storage Users → ApprovalWorkflowLevels. Pattern Mig 29 ADD-DROP no-BACKFILL Option A (accept lose 4 prod user value `fin.pp` + `pm.nv` + `nv.test` + `truong.nguyen`). Cookie-cutter 6 BE file (User.cs -1 prop + ApprovalWorkflow.cs +1 prop `AllowApproverSkipToFinal` per-Approver-slot + ApprovalWorkflowConfiguration.cs +HasDefaultValue + PurchaseEvaluationWorkflowService.cs surgical -37 LOC F2 Drafter SUBMIT branch line 121-157 stub + Mig 3-file). TransitionAsync `bool skipToFinal` 8th param KEPT cho K2 repurpose APPROVE STEP. 4 Application compile-break sites (UserFeatures.cs LIST + GET DTO mapping + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures.cs drafter flag = false) patched với sentinel `false` + K2 marker comment (DTO/Command signature unchanged per spec — K2 sẽ refactor). Mig 31 Up() manual reorder ADD-DROP correct (no BACKFILL). Both DBs Dev + Design applied successful. Build production projects clean 0 err 0 warn. Test compile error `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` left for K7 chunk (spec exclude test scope). Pattern `feedback_per_nv_permission_scope.md` reinforced 3× cumulative (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2-refactor). UserConfiguration.cs file không tồn tại — User entity configured inline `ApplicationDbContext.OnModelCreating` ~line 86, không có HasDefaultValue cho `AllowDrafterSkipToFinal`, EF picks prop change tự động.
|
||||
- **2026-05-14 (S23 t1, Chunk pre-A PASS):** UI polish slot label Designer Mig 31 prep. File `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` line 873 replaced `Quyền duyệt NV #{ei + 1}` → `Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}`. Pattern: lookup user fullName từ `usersList` query (Option A — DTO `EditLevelEntry` không có `approverFullName` field, chỉ `LevelDto` BE response có `approverUserName` nhưng edit state dùng `EditLevelEntry`). `usersList` đã in scope ~line 500, pattern lookup `.find(x => x.id === e.approverUserId)` đã dùng tại line 535 cho validation. Cookie-cutter 1 file fe-admin only (fe-user KHÔNG có Designer per Investigator K0 S1). Tailwind classes preserved (`mb-1 text-[10px] font-medium uppercase text-amber-700`). Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395 KB (unchanged trivial). Token ~5k.
|
||||
- **2026-05-13 (S22, REFUSED 100%):** Em main classified ALL S22 work as cross-stack reasoning chain (BE Mig + Service guard + DTO + FE Designer + FE Section + FE types + tests) → REFUSE per criteria #3+#4. Em main solo executed. State chốt S22: **30 migrations** (+1 Mig 30 AllowApproverEditSection1 per-NV F4 flag), **104 test PASS** (+20 từ 84 — gồm PE WF ReturnMode + Draft guard + Reflection-based Authorize policy regression), ~146 endpoints (+3), 46 gotchas unchanged, 33 active prod users (13 cũ + 20 mới S22+2). 7 patterns successfully applied throughout S22 (validated continued effectiveness): Pattern 7 per-NV admin opt-in flag (Mig 30 follow Mig 29), Pattern 2 EF migration 3-file rule, Pattern 8 tách endpoint narrow scope (AdjustBudget vs UpdatePeDraft), Pattern 9 defense-in-depth FE+BE guard pair (S22+1 disable 3 button), Pattern 10 Reflection-based regression test cho Authorize policy (Plan C task 4 #44, 5 test ~50 LOC), Pattern 11 test infra helper cookie-cutter (SeedWorkflowAsync + SeedApproversAsync), Pattern 12 InternalsVisibleTo csproj expose internal helper cho test. Mismatches discovered S22: (1) "Đang trong quá trình duyệt = người điều chỉnh cũng là người duyệt" — em first interpret default Approver scope always allowed → bro corrected per-NV admin opt-in flag (Mig 30). Lesson: clarify default behavior vs admin opt-in TRƯỚC khi default scope expansion. (2) `PE.changelogs` field KHÔNG có trong PeDetailBundle — em first design history display trong BudgetAdjustSection, build FAIL TS2339. Fix: removed history display (defer S23+ via separate fetch endpoint). (3) Dialog `size="xl"` NOT supported — only "sm" | "md" | "lg". Use "lg" cho preview iframe. (4) API auth field `accessToken` không phải `token`. Script `seed-test-users-prod.ps1` lần đầu FAIL 401 sau auth — em fix `$authResp.accessToken`.
|
||||
- **2026-05-13 (S21 t3-t5, REFUSED 3×):** Em main classified all 3 turns as cross-stack reasoning chain (BE+FE+test tightly coupled) → REFUSE per criteria #3+#4 (cross-stack > 2 layers, bug fix reasoning chain). Bug fix gotcha #45 = bug + reasoning, F1+F2+F3 = schema design decision, Refactor per-NV = drastic refactor schema + Service + FE × 2 app. All correct REFUSE — em main solo executed. Strict scope criteria validated S21 t3-t5 — REFUSE rate 100% match Anthropic warning "tightly interdependent coding". Cumulative: 84 test, 29 mig, 45 gotcha. Pattern saved future invocation: per-NV permission scope split natural theo role + EF migration BACKFILL reorder pattern.
|
||||
|
||||
@ -728,15 +728,11 @@ 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 29 (S21 t5) — F2 drafter flag từ User entity (per-Drafter user
|
||||
// AllowDrafterSkipToFinal). Default false nếu DrafterUserId null.
|
||||
// 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;
|
||||
if (e.DrafterUserId is Guid drafterId)
|
||||
{
|
||||
var drafterUser = await userManager.Users.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.Id == drafterId, ct);
|
||||
drafterAllowSkipToFinal = drafterUser?.AllowDrafterSkipToFinal ?? false;
|
||||
}
|
||||
|
||||
string? awCode = null, awName = null;
|
||||
int? awVersion = null;
|
||||
|
||||
@ -62,7 +62,9 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IApplicationDb
|
||||
var roles = await userManager.GetRolesAsync(u);
|
||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
||||
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal));
|
||||
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels.
|
||||
// DTO field kept transiently (K2 sẽ refactor DTO + drop field). Sentinel false.
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, false));
|
||||
}
|
||||
|
||||
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||
@ -84,7 +86,9 @@ public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbCo
|
||||
string? deptName = null;
|
||||
if (u.DepartmentId is { } did)
|
||||
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal);
|
||||
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels.
|
||||
// DTO field kept transiently (K2 sẽ refactor DTO + drop field). Sentinel false.
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,11 +340,11 @@ public class SetUserAllowDrafterSkipToFinalCommandHandler(UserManager<User> user
|
||||
{
|
||||
public async Task Handle(SetUserAllowDrafterSkipToFinalCommand request, CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(request.Id.ToString())
|
||||
// Mig 31 (S23 t1 K1) — F2 storage moved to ApprovalWorkflowLevels.AllowApproverSkipToFinal
|
||||
// (per-Approver slot, set via Workflow Designer). Command kept transiently NoOp until K2
|
||||
// sẽ replace với SetApprovalWorkflowLevelAllowApproverSkipToFinalCommand + FE remove toggle.
|
||||
_ = await userManager.FindByIdAsync(request.Id.ToString())
|
||||
?? throw new NotFoundException("User", request.Id);
|
||||
user.AllowDrafterSkipToFinal = request.AllowDrafterSkipToFinal;
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,5 +104,13 @@ public class ApprovalWorkflowLevel : BaseEntity
|
||||
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
|
||||
public bool AllowApproverEditBudget { get; set; }
|
||||
|
||||
/// F2 (Mig 31 — S23 t1 Plan K) — REFACTOR semantic + storage từ
|
||||
/// `Users.AllowDrafterSkipToFinal`. Cho phép NV slot này (khi đang duyệt
|
||||
/// ChoDuyet) Approve skip thẳng Cấp cuối, bỏ qua mọi Bước/Cấp trung gian
|
||||
/// còn lại. Default false (admin opt-in per slot). Mirror F3+F4 admin opt-in
|
||||
/// per-Approver pattern (Mig 29 + Mig 30) reinforced 3× cumulative.
|
||||
/// NO BACKFILL — 4 prod user lose value cũ per bro Option A.
|
||||
public bool AllowApproverSkipToFinal { get; set; }
|
||||
|
||||
public ApprovalWorkflowStep? Step { get; set; }
|
||||
}
|
||||
|
||||
@ -28,12 +28,9 @@ public class User : IdentityUser<Guid>
|
||||
// được sub-step đó. Null cho admin/system/external user.
|
||||
public PositionLevel? PositionLevel { get; set; }
|
||||
|
||||
// Mig 29 (Session 21 turn 5) — F2 per-Drafter: cho phép user này (khi đóng
|
||||
// vai Drafter) gửi PE thẳng Cấp cuối, skip mọi Bước/Cấp trung gian. Workspace
|
||||
// hiện checkbox "Gửi thẳng Cấp cuối" conditional theo flag này.
|
||||
//
|
||||
// Mặc định false (an toàn). Admin set ở User Management page. Backfill
|
||||
// Mig 29: bulk set TRUE cho user nào từng Drafter PE link workflow có
|
||||
// workflow.AllowDrafterSkipToFinal=true (preserve admin config S21 t4).
|
||||
public bool AllowDrafterSkipToFinal { get; set; }
|
||||
// Mig 31 (S23 t1 Plan K Chunk A) — F2 semantic + storage REFACTOR:
|
||||
// Drafter-skip-from-Nháp (cũ Mig 29) → Approver-skip-during-ChoDuyet (mới).
|
||||
// Cờ chuyển sang `ApprovalWorkflowLevels.AllowApproverSkipToFinal` per-slot
|
||||
// Approver (mirror F3+F4 admin opt-in per-Approver pattern). NO BACKFILL —
|
||||
// 4 prod user accept lose value per bro Option A, admin re-config qua Designer.
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user