[CLAUDE] PurchaseEvaluation · FE-User · FE-Admin · Tests: PE note "Duyệt là KẾT THÚC quy trình (không trình CEO)" — 3 surface
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m58s

Phiếu mà quy trình duyệt có cấp bật AllowApproverFinalize (Mig 58) sẽ kết thúc tại cấp đó,
KHÔNG trình CEO. Hiển thị note cho MỌI NGƯỜI NẮM ở 3 surface (anh Kiệt FDC). KHÔNG migration
(cột AllowApproverFinalize đã có Mig 58, prod ở Mig 59).

BE (3 file, computed DTO):
- PurchaseEvaluationDtos.cs: +bool EndsBeforeCeo +string? FinalizeStepName +string? FinalizeLevelName
  trên DetailBundle + ListItem; flow-level +bool AllowApproverFinalize.
- PurchaseEvaluationFeatures.cs: detail compute first-reachable (Step.Order↑→Level.Order↑, dept-name
  in-memory); list + inbox projection qua 3 subquery EF-safe (lv.Step!.Name). V1 (workflowId==null) → false.
- CreateContractFromEvaluationFeatures.cs: ListApproved projection cùng DTO → +3 subquery y hệt.

FE 2-app (fe-user + fe-admin SHA-mirror component):
- PeFinalizeChip.tsx (MỚI, chip tím) ở 3 danh sách: PeListPanel + PurchaseEvaluationsListPage (cây) + InboxPage.
- PeWorkflowPanel.tsx: note callout trong phiếu, branch DaDuyet (past-tense) / đang-chạy; copy boundary-aware
  "...là KẾT THÚC, không trình CEO. Các cấp trước vẫn duyệt như thường." (tránh cấp giữa hiểu nhầm).
- UserDashboardPage.tsx (chỉ fe-user): dải "Phiếu Duyệt NCC gần đây" + chip (admin dashboard để nguyên).
- types: PeListItem/PeDetailBundle/PeApprovalFlowLevel +field.

Tests: +PeFinalizeProjectionTests (EF-translate smoke 3 subquery + first-reachable + V1-false). Full suite 421→422.
em-main self-gate sau workflow (impl-frontend return rỗng #53 → mirror fe-admin + verify 5 site recover-disk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-25 14:58:54 +07:00
parent a71753bde8
commit f0bca7737c
16 changed files with 417 additions and 6 deletions

View File

@ -0,0 +1,114 @@
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.PurchaseEvaluations;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
using SolutionErp.Domain.PurchaseEvaluations;
using SolutionErp.Infrastructure.Tests.Common;
namespace SolutionErp.Infrastructure.Tests.Application;
// [S89 anh Kiệt FDC] PE finalize-note (Mig 58 AllowApproverFinalize) — TRANSLATION-SMOKE
// cho 3 subquery mới (EndsBeforeCeo / FinalizeStepName / FinalizeLevelName) trong list
// projection. Subquery: (from lv in db.ApprovalWorkflowLevels where lv.AllowApproverFinalize
// && lv.Step!.ApprovalWorkflowId == e.ApprovalWorkflowId orderby lv.Step!.Order, lv.Order
// select lv.Step!.Name / lv.Name).FirstOrDefault() — PHẢI translate trên EF SQLite (rủi ro
// reviewer cảnh báo). Target = ListApprovedPurchaseEvaluationsQuery (sạch nhất: chỉ
// Phase==DaDuyet, KHÔNG role-paging) — cùng subquery xuất hiện ở List + Inbox projection.
// Verify: translate (không ném) + first-reachable (Step.Order↑→Level.Order↑) + V1 null→false.
// IdentityFixture: ApproverUserId là Guid non-null (FK) → cần 1 User row thật.
public class PeFinalizeProjectionTests
{
private static ApprovalWorkflow BuildWorkflow(
string code, Guid approverUserId,
params (string stepName, (string levelName, bool finalize)[] levels)[] steps)
{
var wf = new ApprovalWorkflow
{
Code = code,
Version = 1,
ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
Name = code,
IsActive = true,
IsUserSelectable = true,
};
for (var s = 0; s < steps.Length; s++)
{
var step = new ApprovalWorkflowStep
{
ApprovalWorkflowId = wf.Id,
Order = s + 1,
Name = steps[s].stepName,
};
for (var l = 0; l < steps[s].levels.Length; l++)
{
var (lvName, fin) = steps[s].levels[l];
step.Levels.Add(new ApprovalWorkflowLevel
{
ApprovalWorkflowStepId = step.Id,
Order = l + 1,
Name = lvName,
ApproverUserId = approverUserId,
AllowApproverFinalize = fin,
});
}
wf.Steps.Add(step);
}
return wf;
}
private static Guid SeedApprovedPe(TestApplicationDbContext db, Guid projectId, Guid? wfId, string code)
{
var id = Guid.NewGuid();
db.PurchaseEvaluations.Add(new PurchaseEvaluation
{
Id = id,
MaPhieu = code,
Type = PurchaseEvaluationType.DuyetNcc,
Phase = PurchaseEvaluationPhase.DaDuyet, // lọt ListApproved filter
TenGoiThau = $"Gói {code}",
ProjectId = projectId,
ContractId = null, // chưa gen HĐ → lọt picker
DrafterUserId = null,
ApprovalWorkflowId = wfId,
});
return id;
}
[Fact]
public async Task ListApproved_FinalizeFields_Translate_FirstReachable_V1False()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var approver = await fix.CreateUserAsync("fin-approver@test.local", "Approver", null, new[] { AppRoles.CostControl });
var project = new Project { Id = Guid.NewGuid(), Code = "FINP", Name = "Dự án Finalize" };
db.Projects.Add(project);
// Bước 1 (Cấp 1, no-flag) → Bước 2 (Cấp 1, FINALIZE). first-reachable = Bước 2 · Cấp 1.
var wfFin = BuildWorkflow("QT-FIN", approver.Id,
("Bước 1", new[] { ("Cấp 1", false) }),
("Bước 2", new[] { ("Cấp 1", true) }));
db.ApprovalWorkflows.Add(wfFin);
// Không cấp nào finalize → endsBeforeCeo=false.
var wfPlain = BuildWorkflow("QT-PLAIN", approver.Id, ("Bước 1", new[] { ("Cấp 1", false) }));
db.ApprovalWorkflows.Add(wfPlain);
var peFin = SeedApprovedPe(db, project.Id, wfFin.Id, "PE-FIN");
var pePlain = SeedApprovedPe(db, project.Id, wfPlain.Id, "PE-PLAIN");
var peV1 = SeedApprovedPe(db, project.Id, null, "PE-V1"); // V1 legacy: no workflow
await db.SaveChangesAsync(CancellationToken.None);
var handler = new ListApprovedPurchaseEvaluationsQueryHandler(db);
// Nếu 3 subquery KHÔNG translate → ném InvalidOperationException = FAIL (blocker).
var res = await handler.Handle(new ListApprovedPurchaseEvaluationsQuery(), CancellationToken.None);
var fin = res.Single(x => x.Id == peFin);
fin.EndsBeforeCeo.Should().BeTrue();
fin.FinalizeStepName.Should().Be("Bước 2"); // first-reachable bỏ qua Bước 1 no-flag
fin.FinalizeLevelName.Should().Be("Cấp 1");
res.Single(x => x.Id == pePlain).EndsBeforeCeo.Should().BeFalse();
res.Single(x => x.Id == peV1).EndsBeforeCeo.Should().BeFalse(); // V1 null-workflow tự đúng
}
}