[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
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:
@ -395,6 +395,69 @@ public class ReturnLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Set workflow (pin quy trình cho draft) — Phase 11 P11-A fix S42.
|
||||
// Endpoint riêng PUT /{id}/workflow: chỉ set ApprovalWorkflowId trên draft
|
||||
// Nhap/TraLai (verify ApplicableType). KHÔNG validate field khác (khác UpdateDraft —
|
||||
// tránh 400 khi FE chỉ gửi workflowId). Single-responsibility: chọn quy trình.
|
||||
// =========================================================================
|
||||
|
||||
public record SetLeaveRequestWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;
|
||||
|
||||
public class SetLeaveRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
|
||||
: IRequestHandler<SetLeaveRequestWorkflowCommand>
|
||||
{
|
||||
public async Task Handle(SetLeaveRequestWorkflowCommand req, CancellationToken ct)
|
||||
{
|
||||
var p = await db.LeaveRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
|
||||
if (p is null) throw new NotFoundException("LeaveRequest", req.Id);
|
||||
var isOwner = p.RequesterUserId == cu.UserId;
|
||||
var isAdmin = cu.Roles.Contains("Admin");
|
||||
if (!isOwner && !isAdmin)
|
||||
throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
|
||||
if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
|
||||
throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
|
||||
var wfType = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == req.ApprovalWorkflowId)
|
||||
.Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
|
||||
if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
|
||||
if (wfType.Value != (int)ApprovalWorkflowApplicableType.LeaveRequest)
|
||||
throw new ConflictException("Quy trình duyệt không thuộc loại Đơn nghỉ phép.");
|
||||
p.ApprovalWorkflowId = req.ApprovalWorkflowId;
|
||||
p.UpdatedAt = clock.UtcNow;
|
||||
p.UpdatedBy = cu.UserId;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record SetOtRequestWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;
|
||||
|
||||
public class SetOtRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock)
|
||||
: IRequestHandler<SetOtRequestWorkflowCommand>
|
||||
{
|
||||
public async Task Handle(SetOtRequestWorkflowCommand req, CancellationToken ct)
|
||||
{
|
||||
var p = await db.OtRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
|
||||
if (p is null) throw new NotFoundException("OtRequest", req.Id);
|
||||
var isOwner = p.RequesterUserId == cu.UserId;
|
||||
var isAdmin = cu.Roles.Contains("Admin");
|
||||
if (!isOwner && !isAdmin)
|
||||
throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
|
||||
if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
|
||||
throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
|
||||
var wfType = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == req.ApprovalWorkflowId)
|
||||
.Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
|
||||
if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
|
||||
if (wfType.Value != (int)ApprovalWorkflowApplicableType.OtRequest)
|
||||
throw new ConflictException("Quy trình duyệt không thuộc loại Đơn OT.");
|
||||
p.ApprovalWorkflowId = req.ApprovalWorkflowId;
|
||||
p.UpdatedAt = clock.UtcNow;
|
||||
p.UpdatedBy = cu.UserId;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MODULE B: OtRequest (ApplicableType=6, prefix "DT/OT")
|
||||
// =========================================================================
|
||||
|
||||
@ -744,6 +744,68 @@ public class ReturnVehicleBookingHandler(IApplicationDbContext db, ICurrentUser
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Set workflow (pin quy trình cho draft) — Phase 11 P11-A fix S42.
|
||||
// Endpoint riêng PUT /{id}/workflow: chỉ set ApprovalWorkflowId trên draft
|
||||
// Nhap/TraLai (verify ApplicableType). KHÔNG validate field khác (khác UpdateDraft).
|
||||
// =========================================================================
|
||||
|
||||
public record SetTravelRequestWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;
|
||||
|
||||
public class SetTravelRequestWorkflowHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
|
||||
: IRequestHandler<SetTravelRequestWorkflowCommand>
|
||||
{
|
||||
public async Task Handle(SetTravelRequestWorkflowCommand req, CancellationToken ct)
|
||||
{
|
||||
var p = await db.TravelRequests.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
|
||||
if (p is null) throw new NotFoundException("TravelRequest", req.Id);
|
||||
var isOwner = p.RequesterUserId == currentUser.UserId;
|
||||
var isAdmin = currentUser.Roles.Contains("Admin");
|
||||
if (!isOwner && !isAdmin)
|
||||
throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
|
||||
if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
|
||||
throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
|
||||
var wfType = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == req.ApprovalWorkflowId)
|
||||
.Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
|
||||
if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
|
||||
if (wfType.Value != (int)ApprovalWorkflowApplicableType.TravelRequest)
|
||||
throw new ConflictException("Quy trình duyệt không thuộc loại Đơn công tác.");
|
||||
p.ApprovalWorkflowId = req.ApprovalWorkflowId;
|
||||
p.UpdatedAt = clock.UtcNow;
|
||||
p.UpdatedBy = currentUser.UserId;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record SetVehicleBookingWorkflowCommand(Guid Id, Guid ApprovalWorkflowId) : IRequest;
|
||||
|
||||
public class SetVehicleBookingWorkflowHandler(IApplicationDbContext db, ICurrentUser currentUser, IDateTime clock)
|
||||
: IRequestHandler<SetVehicleBookingWorkflowCommand>
|
||||
{
|
||||
public async Task Handle(SetVehicleBookingWorkflowCommand req, CancellationToken ct)
|
||||
{
|
||||
var p = await db.VehicleBookings.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
|
||||
if (p is null) throw new NotFoundException("VehicleBooking", req.Id);
|
||||
var isOwner = p.RequesterUserId == currentUser.UserId;
|
||||
var isAdmin = currentUser.Roles.Contains("Admin");
|
||||
if (!isOwner && !isAdmin)
|
||||
throw new ForbiddenException("Chỉ người tạo hoặc Admin được chọn quy trình.");
|
||||
if (p.Status != WorkflowAppStatus.Nhap && p.Status != WorkflowAppStatus.TraLai)
|
||||
throw new ConflictException("Chỉ chọn quy trình khi trạng thái Nháp hoặc Trả lại.");
|
||||
var wfType = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == req.ApprovalWorkflowId)
|
||||
.Select(w => (int?)w.ApplicableType).FirstOrDefaultAsync(ct);
|
||||
if (wfType is null) throw new NotFoundException("ApprovalWorkflow", req.ApprovalWorkflowId);
|
||||
if (wfType.Value != (int)ApprovalWorkflowApplicableType.VehicleBooking)
|
||||
throw new ConflictException("Quy trình duyệt không thuộc loại Đặt xe.");
|
||||
p.ApprovalWorkflowId = req.ApprovalWorkflowId;
|
||||
p.UpdatedAt = clock.UtcNow;
|
||||
p.UpdatedBy = currentUser.UserId;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Shared CodeGen helper — Prefix-keyed WorkflowAppCodeSequences (SERIALIZABLE tx).
|
||||
// Mirror SubmitProposalHandler.GenerateMaDeXuatAsync. Format: {prefix}/{seq:D3}.
|
||||
|
||||
Reference in New Issue
Block a user