[CLAUDE] Workflow: fix workflow picker 2 bug (P11-A Max re-review) + SetWorkflow endpoint
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m5s

Double-check chất lượng P11-A ở Max (agents trước chạy High + truncate 3×) →
phát hiện 2 bug THẬT trong workflow-picker FE của WorkflowAppDetailPage (core
approve/reject/return ĐÚNG, chỉ sub-flow chọn quy trình hỏng):

Bug #1 (HIGH) — pinWorkflow PUT /{id} chỉ gửi {approvalWorkflowId} → UpdateDraft
  validator (Reason NotEmpty, NumDays>0...) fail → 400. Nút "Lưu quy trình" vỡ.
Bug #2 (HIGH) — fetch workflow expect flat array nhưng endpoint trả
  AwAdminOverviewDto {types:[...]} → picker rỗng/crash. FE copy nhầm pattern hỏng
  của ProposalCreatePage thay vì PE/Contract proven.

Fix:
- BE: thêm endpoint chuyên dụng PUT /{id}/workflow + Set{Module}WorkflowCommand/Handler
  cho 4 module — chỉ set ApprovalWorkflowId trên draft Nhap/TraLai (verify ApplicableType
  per module), KHÔNG validate field khác. Single-responsibility, bulletproof.
- FE: sửa fetch mirror PE/Contract (data.types.find(t=>t.applicableType===X)?.history
  .filter(isUserSelectable)) + pin gọi endpoint mới. fe-admin+fe-user SHA256 identical.
- Test: +3 SetWorkflow (happy no-status-change / wrong ApplicableType Conflict / submitted
  guard) → 141→144 PASS.

Verify: BE build 0 error · 144 test PASS · FE build ×2 · SHA256 identical.
Bonus phát hiện: ProposalCreatePage (S37) có bug #2 có sẵn (latent, chưa exercise UAT)
  → flag spawn task riêng, KHÔNG fix trong commit này.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-30 10:14:34 +07:00
parent e7b66cd52b
commit 75df04ec82
11 changed files with 302 additions and 31 deletions

View File

@ -440,4 +440,83 @@ public class WorkflowAppApproveV2Tests
opinions[0].Comment.Should().Be("(duyệt — không ý kiến)");
}
}
// ============ SetWorkflow (P11-A fix S42): pin quy trình cho draft ============
// Endpoint riêng /workflow — KHÔNG validate field khác (fix FE bug PUT /{id} partial → 400).
[Fact]
public async Task SetWorkflow_OnDraft_SetsApprovalWorkflowId_NoStatusChange()
{
var (fix, db, clock) = NewCtx();
using (fix)
{
var requester = await fix.CreateUserAsync("req-sw1@test.local", "Requester", null, Array.Empty<string>());
var approver = await fix.CreateUserAsync("ap-sw1@test.local", "Approver", null, Array.Empty<string>());
var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);
var leave = BuildLeave(requester.Id, workflowId: null, WorkflowAppStatus.Nhap, currentLevel: null);
db.LeaveRequests.Add(leave);
await db.SaveChangesAsync(CancellationToken.None);
await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
.Handle(new SetLeaveRequestWorkflowCommand(leave.Id, wf.Id), CancellationToken.None);
leave.ApprovalWorkflowId.Should().Be(wf.Id);
leave.Status.Should().Be(WorkflowAppStatus.Nhap, "set workflow KHÔNG đổi trạng thái");
}
}
[Fact]
public async Task SetWorkflow_WrongApplicableType_ThrowsConflict_DoesNotPin()
{
var (fix, db, clock) = NewCtx();
using (fix)
{
var requester = await fix.CreateUserAsync("req-sw2@test.local", "Requester", null, Array.Empty<string>());
// Workflow loại OtRequest (=6) — KHÔNG khớp LeaveRequest (=5)
var otWf = new ApprovalWorkflow
{
Id = Guid.NewGuid(),
Code = "QT-OT-X",
Version = 1,
Name = "OT workflow",
ApplicableType = ApprovalWorkflowApplicableType.OtRequest,
IsActive = true,
IsUserSelectable = true,
};
db.ApprovalWorkflows.Add(otWf);
var leave = BuildLeave(requester.Id, workflowId: null, WorkflowAppStatus.Nhap, currentLevel: null);
db.LeaveRequests.Add(leave);
await db.SaveChangesAsync(CancellationToken.None);
var act = async () => await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
.Handle(new SetLeaveRequestWorkflowCommand(leave.Id, otWf.Id), CancellationToken.None);
await act.Should().ThrowAsync<ConflictException>().WithMessage("*Đơn nghỉ phép*");
leave.ApprovalWorkflowId.Should().BeNull("guard chặn trước khi pin");
}
}
[Fact]
public async Task SetWorkflow_WhenAlreadySubmitted_ThrowsConflict()
{
var (fix, db, clock) = NewCtx();
using (fix)
{
var requester = await fix.CreateUserAsync("req-sw3@test.local", "Requester", null, Array.Empty<string>());
var approver = await fix.CreateUserAsync("ap-sw3@test.local", "Approver", null, Array.Empty<string>());
var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id);
var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1);
db.LeaveRequests.Add(leave);
await db.SaveChangesAsync(CancellationToken.None);
var act = async () => await new SetLeaveRequestWorkflowHandler(db, AsUser(requester), clock)
.Handle(new SetLeaveRequestWorkflowCommand(leave.Id, wf.Id), CancellationToken.None);
await act.Should().ThrowAsync<ConflictException>().WithMessage("*Nháp hoặc Trả lại*");
}
}
}