[CLAUDE] Contract: K8-blocker Lines-editor KHKK — gan hang muc SP-002 cho dong (command hua-trong-comment chua tung land)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m27s

- Dry-run K8 bat TRUOC gio chay: auto-gen line moi CatalogEntryId=null + comment hen "UpdateContractSigningPlanLineCommand" (K4b) nhung K4b that ship ?group= => 0 impl/0 route/0 FE; submit-guard va-5 chan null => MOI phieu KHKK tao sau K2 ket Nhap vinh vien (dung cho cicd-K2 khai "chan-ly-rong")
- BE: UpdateContractSigningPlanLineCommand(PlanId,LineId,CatalogEntryId) — EnsureDraftEditable + rao ContractId!=null 409 (gate F-6) + fail-fast entry song/IsActive/dung-nhom 409 + denorm TenHangMuc + changelog EntityType.Line EntityId=line.Id + ten NCC vao Summary (gate F-1)
- API: PUT /contract-signing-plans/{id}/lines/{lineId} policy KeHoachKyKet.Update + UpdateSigningPlanLineBody
- FE x2 app KhkkDetailPage (SHA-pair 0d6147d0): select gan tai bang (Nhap/TraLai, khoa dong da bac cau HD) + option-fallback giu hang-muc-da-luu khi vang options (gate F-2, 3 ca loading/error/ngung-hieu-luc) + dong loi query + amber nhom-0-hang-muc (F-3) + aria-label per-dong (F-8)
- Test +5 Fact: duong gan->submit-QUA guard that · khac-nhom 409 · phase 409 · IsActive=false 409 · gan-DE doi ca Id lan snapshot (gate F-4)
- Gate PASS-WITH-FLAGS-8/0-blocker — disposition tung dong trong sub-reviewer-gate-lineeditor.md; F-5/F-7 ghi no cung goi RowVersion/unique voi no K7 race-2-request
- scripts/backup-sql.ps1 default D:\ -> C:\ (VPS khong co o D — do that B0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-01 17:08:47 +07:00
parent 7d0867011c
commit 0bfadc2011
6 changed files with 392 additions and 11 deletions

View File

@ -900,4 +900,144 @@ public class ContractSigningPlanGroupAndCatalogTests
.Select(p => new PlanScope(p.MaKeHoach!, p.ApprovalGroup, p.ApprovalWorkflowId!.Value))
.ToListAsync(CancellationToken.None);
}
// ===================================================================
// [S168 K8-blocker] Lines-editor `UpdateContractSigningPlanLineCommand` — mảnh comment
// auto-gen HẸN cho K4b nhưng chưa từng land, dry-run K8 bắt: submit-guard vá-5 chặn
// line null mà 0 đường gán ⇒ phiếu mới kẹt Nháp vĩnh viễn. 3 phép đo: DƯƠNG đi trọn
// (gán → submit QUA — nối thẳng vào guard thật, không đo gán suông) · ÂM sai nhóm ·
// ÂM sai phase. Handler THẬT, không mock (khuôn Act-helpers file này).
// ===================================================================
private static async Task AssignLineAsync(
KhkkGroupFixture f, TestCurrentUser actor, Guid planId, Guid lineId, Guid catalogId)
{
await using var db = f.NewDb(actor);
var handler = new UpdateContractSigningPlanLineCommandHandler(db, actor);
await handler.Handle(
new UpdateContractSigningPlanLineCommand(planId, lineId, catalogId), CancellationToken.None);
}
[Fact]
public async Task GanHangMuc_DungNhom_SnapshotTen_RoiTrinhQuaGuard()
{
using var f = new KhkkGroupFixture();
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
var approver = await AddUserAsync(f, "NV duyệt Cấp 1");
var wf = await SeedWorkflowAsync(f, "QT-KHKK-LINE-A", steps: [[[approver.Id]]]);
var catalog = await AddCatalogAsync(f, approvalGroup: 4, tenVi: "Xi măng");
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/101",
ContractSigningPlanPhase.DangSoanThao, approvalGroup: 4,
lines: [new LineSeed(100m, 100m, CatalogId: null)]);
var lineId = (await ReadPlanAsync(f, planId)).Lines.Single().Id;
await AssignLineAsync(f, drafter, planId, lineId, catalog);
var line = (await ReadPlanAsync(f, planId)).Lines.Single();
line.CatalogEntryId.Should().Be(catalog);
line.TenHangMuc.Should().Be("Xi măng",
"tên hạng mục phải denorm TẠI THỜI ĐIỂM GÁN (khuôn SignedByFullName Mig 26)");
// Nối trọn vào submit-guard THẬT: gán xong thì trình phải QUA (ca dương của vá-5,
// chống thế giới nơi gán ăn nhưng submit vẫn đỏ vì lý do khác).
await ActAsync(f, drafter, planId, "submit");
(await ReadPlanAsync(f, planId)).Phase.Should().Be(ContractSigningPlanPhase.ChoDuyet);
}
[Fact]
public async Task GanHangMuc_KhacNhomDuyet_BiChan409_LineGiuNguyen()
{
using var f = new KhkkGroupFixture();
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
// ApproverUserId cần ROW Users THẬT (FK Restrict — ghi chú fixture đầu file).
var approver = await AddUserAsync(f, "NV duyệt Cấp 1");
var wf = await SeedWorkflowAsync(f, "QT-KHKK-LINE-B", steps: [[[approver.Id]]]);
var catalogN2 = await AddCatalogAsync(f, approvalGroup: 2, tenVi: "Nhân công cốp pha");
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/102",
ContractSigningPlanPhase.DangSoanThao, approvalGroup: 4,
lines: [new LineSeed(100m, 100m, CatalogId: null)]);
var lineId = (await ReadPlanAsync(f, planId)).Lines.Single().Id;
var act = () => AssignLineAsync(f, drafter, planId, lineId, catalogN2);
await act.Should().ThrowAsync<ConflictException>(
"hạng mục nhóm N2 không được gán vào phiếu nhóm N4 — fail-fast tầng-gán, "
+ "submit-guard chỉ là chốt cuối");
(await ReadPlanAsync(f, planId)).Lines.Single().CatalogEntryId.Should().BeNull(
"gán fail thì line phải GIỮ NGUYÊN — không ghi dở");
}
[Fact]
public async Task GanHangMuc_EntryNgungHieuLuc_BiChan409()
{
using var f = new KhkkGroupFixture();
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
var approver = await AddUserAsync(f, "NV duyệt Cấp 1");
var wf = await SeedWorkflowAsync(f, "QT-KHKK-LINE-D", steps: [[[approver.Id]]]);
// Entry ĐÚNG nhóm nhưng IsActive=false — nhánh guard còn lại (gate-lineeditor F-4).
Guid inactive;
{
await using var db = f.NewDb(TestCurrentUser.System());
var entry = new ContractCatalogEntry
{
Id = Guid.NewGuid(), Code = "K2-" + Guid.NewGuid().ToString("N")[..8],
TenVi = "Hạng mục đã ngừng", GroupCode = "B1", ApprovalGroup = 4,
SignerRole = "TP.PRO", IsActive = false, SortOrder = 1,
};
db.ContractCatalogEntries.Add(entry);
await db.SaveChangesAsync(CancellationToken.None);
inactive = entry.Id;
}
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/104",
ContractSigningPlanPhase.DangSoanThao, approvalGroup: 4,
lines: [new LineSeed(100m, 100m, CatalogId: null)]);
var lineId = (await ReadPlanAsync(f, planId)).Lines.Single().Id;
var act = () => AssignLineAsync(f, drafter, planId, lineId, inactive);
await act.Should().ThrowAsync<ConflictException>(
"entry IsActive=false phải bị chặn dù ĐÚNG nhóm — nhánh guard độc lập với nhánh khác-nhóm");
}
[Fact]
public async Task GanHangMuc_GanDe_DoiCaIdLanTenSnapshot()
{
using var f = new KhkkGroupFixture();
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
var approver = await AddUserAsync(f, "NV duyệt Cấp 1");
var wf = await SeedWorkflowAsync(f, "QT-KHKK-LINE-E", steps: [[[approver.Id]]]);
var catA = await AddCatalogAsync(f, approvalGroup: 4, tenVi: "Xi măng");
var catB = await AddCatalogAsync(f, approvalGroup: 4, tenVi: "Thép hình");
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/105",
ContractSigningPlanPhase.DangSoanThao, approvalGroup: 4,
lines: [new LineSeed(100m, 100m, CatalogId: null)]);
var lineId = (await ReadPlanAsync(f, planId)).Lines.Single().Id;
await AssignLineAsync(f, drafter, planId, lineId, catA);
// Gán ĐÈ là đường hợp lệ (người soạn chọn nhầm rồi chọn lại) — cả Id lẫn snapshot đổi theo.
await AssignLineAsync(f, drafter, planId, lineId, catB);
var line = (await ReadPlanAsync(f, planId)).Lines.Single();
line.CatalogEntryId.Should().Be(catB);
line.TenHangMuc.Should().Be("Thép hình", "snapshot phải đổi theo lần gán CUỐI");
}
[Fact]
public async Task GanHangMuc_PhieuDaTrinh_BiChanPhase()
{
using var f = new KhkkGroupFixture();
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
// ApproverUserId cần ROW Users THẬT (FK Restrict — ghi chú fixture đầu file).
var approver = await AddUserAsync(f, "NV duyệt Cấp 1");
var wf = await SeedWorkflowAsync(f, "QT-KHKK-LINE-C", steps: [[[approver.Id]]]);
var catalog = await AddCatalogAsync(f, approvalGroup: 4, tenVi: "Thép hình");
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/103",
ContractSigningPlanPhase.ChoDuyet, approvalGroup: 4,
lines: [new LineSeed(100m, 100m, CatalogId: null)],
stepIndex: 0, levelOrder: 1);
var lineId = (await ReadPlanAsync(f, planId)).Lines.Single().Id;
var act = () => AssignLineAsync(f, drafter, planId, lineId, catalog);
await act.Should().ThrowAsync<ConflictException>(
"phiếu đã trình thì dòng bất biến — chỉ Nháp/Trả lại sửa được (EnsureDraftEditable)");
}
}