[CLAUDE] Workflow: W1 gỡ 2 bom per-type type-3 — deactivate per-Code (R-6 tập tường minh) + overview family; test-before RED→GREEN +4 test (672/672)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -82,7 +82,22 @@ public record AwDefinitionDto(
|
||||
public record AwTypeSummaryDto(
|
||||
int ApplicableType,
|
||||
string ApplicableTypeLabel,
|
||||
// `Active` = bản đang áp dụng ĐẦU TIÊN của loại (giữ nguyên nghĩa từ Mig 22 — FE đọc
|
||||
// thẳng ô này: ApprovalWorkflowsV2Page.tsx thẻ landing + panel "đang áp dụng").
|
||||
// ⚠️ Với loại per-Code-family, đây chỉ là MỘT trong N bản đang áp dụng (bản Version cao
|
||||
// nhất gặp trước), KHÔNG đại diện cho `Code` nào cả ⇒ panel per-Code phải tự cắt theo
|
||||
// `Code` (KHKK đang làm vậy: ApprovalWorkflowsV2Page.tsx:321-339), đừng đọc ô này.
|
||||
AwDefinitionDto? Active,
|
||||
// [S187 W1b] Các bản ĐANG ÁP DỤNG CÒN LẠI của loại — KHÔNG lặp lại bản trong `Active`.
|
||||
// Tập đầy đủ những bản đang áp dụng = [Active] ∪ ActiveOthers.
|
||||
//
|
||||
// Vì sao cần: loại per-Code-family có NHIỀU quy trình cùng IsActive trong CÙNG một
|
||||
// ApplicableType (type-10 KHKK 8 nhóm N1..N8 · type-3 HĐ 7 loại). Trước bản này overview
|
||||
// chỉ trả 1 và N-1 bản còn lại chỉ còn đường ra qua `History` — mà `History` là KHO PHIÊN
|
||||
// BẢN, FE dựng mục "phiên bản cũ" bằng `history.filter(d => !d.isActive)` ⇒ chúng KHÔNG
|
||||
// xuất hiện ở đâu trên màn hình. Loại 1-active-per-type thì danh sách này RỖNG (không đổi gì).
|
||||
List<AwDefinitionDto> ActiveOthers,
|
||||
// KHO PHIÊN BẢN — MỌI version của loại, kể cả bản đang áp dụng (không lọc `!IsActive`).
|
||||
List<AwDefinitionDto> History);
|
||||
|
||||
public record AwAdminOverviewDto(List<AwTypeSummaryDto> Types);
|
||||
@ -206,10 +221,26 @@ public class GetAwAdminOverviewQueryHandler(
|
||||
.Select(type =>
|
||||
{
|
||||
var versions = definitions.Where(d => d.ApplicableType == type).Select(ToDto).ToList();
|
||||
|
||||
// 🔴 [S187 W1b] Trả ĐỦ mọi bản đang áp dụng của loại, không chỉ bản đầu.
|
||||
//
|
||||
// Loại per-Code-family (type-10 KHKK 8 nhóm · type-3 HĐ 7 loại) có N bản cùng
|
||||
// IsActive; bản trước lấy `versions.FirstOrDefault(v => v.IsActive)` ⇒ N-1 bản kia
|
||||
// rơi hết vào `History`, nơi FE lọc `!isActive` để dựng danh sách phiên bản cũ ⇒
|
||||
// chúng biến mất khỏi UI, im lặng.
|
||||
//
|
||||
// Chia 2 ô thay vì gộp 1: `Active` PHẢI giữ nguyên giá trị cũ (FE đang đọc thẳng),
|
||||
// nên `ActiveOthers` gánh phần còn lại — hợp của 2 ô = trọn tập, KHÔNG phần tử nào
|
||||
// xuất hiện hai lần. Không hỏi ApplicableType ở đây là CỐ Ý: luật là "có mấy bản
|
||||
// đang áp dụng thì trả bấy nhiêu", tự đúng cho loại family thêm sau này; loại
|
||||
// 1-active-per-type giữ y hệt hành vi cũ (`ActiveOthers` rỗng).
|
||||
var actives = versions.Where(v => v.IsActive).ToList();
|
||||
|
||||
return new AwTypeSummaryDto(
|
||||
(int)type,
|
||||
AwLabels.Type.GetValueOrDefault(type, type.ToString()),
|
||||
versions.FirstOrDefault(v => v.IsActive),
|
||||
actives.FirstOrDefault(),
|
||||
actives.Skip(1).ToList(),
|
||||
versions);
|
||||
})
|
||||
.ToList();
|
||||
@ -328,6 +359,22 @@ public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefi
|
||||
public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<CreateAwDefinitionCommand, Guid>
|
||||
{
|
||||
// 🔴 [S187 W1a] Những loại tắt bản active theo phạm vi **per-CODE** — TẬP TYPE TƯỜNG MINH.
|
||||
//
|
||||
// Tiêu chí vào danh sách: loại đó có NHIỀU quy trình sống song song trong CÙNG một
|
||||
// ApplicableType, mỗi cái một `Code` riêng, và mỗi cái phải tự giữ bản active của mình:
|
||||
// • type-10 `ContractSigningPlan` (KHKK) — 8 nhóm duyệt `KHKK-N1..N8`
|
||||
// (`DbInitializer.SeedKhkkGroupWorkflowsAsync`, seed cả 8 với IsActive=1);
|
||||
// • type-3 `Contract` (HĐ) — 7 loại hợp đồng, mỗi loại một quy trình riêng.
|
||||
//
|
||||
// Thêm loại vào đây = quyết định về NGỮ NGHĨA của loại đó, không phải về hình dạng mã:
|
||||
// mọi `Code` của loại đều đi nhánh per-Code, kể cả mã ngoài họ (R-6).
|
||||
private static readonly ApprovalWorkflowApplicableType[] PerCodeDeactivationTypes =
|
||||
[
|
||||
ApprovalWorkflowApplicableType.ContractSigningPlan,
|
||||
ApprovalWorkflowApplicableType.Contract,
|
||||
];
|
||||
|
||||
public async Task<Guid> Handle(CreateAwDefinitionCommand request, CancellationToken ct)
|
||||
{
|
||||
var typeEnum = (ApprovalWorkflowApplicableType)request.ApplicableType;
|
||||
@ -353,21 +400,26 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
.MaxAsync(w => (int?)w.Version, ct) ?? 0;
|
||||
nextVersion++;
|
||||
|
||||
// Tắt bản đang active — PHẠM VI phụ thuộc loại (bản trước: LUÔN per-type).
|
||||
// Tắt bản đang active — PHẠM VI phụ thuộc loại (bản gốc Mig 22: LUÔN per-type).
|
||||
//
|
||||
// • type-10 `ContractSigningPlan` (KHKK): **per-CODE**. Loại này có 8 quy trình
|
||||
// SỐNG SONG SONG cùng type — mỗi nhóm duyệt N1..N8 một `Code` riêng
|
||||
// (`DbInitializer.SeedKhkkGroupWorkflowsAsync:609`, seed cả 8 với IsActive=1).
|
||||
// Tắt theo type ⇒ tạo version mới cho MỘT nhóm sẽ hạ IsActive của CẢ 7 nhóm còn
|
||||
// lại (mỗi nhóm mất quy trình active ⇒ FE hết auto-pin ⇒ không tạo được phiếu).
|
||||
// • các loại khác: **per-TYPE** như cũ — 1 active/type là bất biến của chúng.
|
||||
// • Loại nằm trong `PerCodeDeactivationTypes` (khai báo + tiêu chí ở ĐẦU class này):
|
||||
// **per-CODE**. Chúng có NHIỀU quy trình SỐNG SONG SONG trong cùng một type, mỗi
|
||||
// cái một `Code`. Tắt theo type ⇒ tạo version mới cho MỘT cái sẽ hạ IsActive của
|
||||
// TẤT CẢ những cái còn lại (mỗi cái mất quy trình active ⇒ FE hết auto-pin ⇒
|
||||
// không lập được phiếu), im lặng và KHÔNG có endpoint bật lại.
|
||||
// • các loại còn lại: **per-TYPE** như cũ — với CHÚNG, 1 active/type vẫn là bất biến.
|
||||
// 🔴 Đây là câu tự-khai từng SAI: bản trước viết "các loại khác: per-TYPE như cũ —
|
||||
// 1 active/type là bất biến của chúng" khi tập per-Code chỉ có type-10, rồi type-3
|
||||
// mọc thêm 7 quy trình `HD-*` sống song song ⇒ câu đó bao luôn type-3 và thành lời
|
||||
// khai sai. Nay câu này neo vào TẬP TYPE tường minh, không neo vào con số.
|
||||
//
|
||||
// 🔴 [S182 R-6] Vị ngữ type-10 KHOÁ đúng 3 vế `Code == request.Code &&
|
||||
// ApplicableType == typeEnum && IsActive`. CẤM thay bằng biến-thể regex
|
||||
// `^KHKK-N[1-8]$`: `Code` type-10 nằm ngoài họ đó (vd `QT-KHKK-V2-001`) sẽ rơi
|
||||
// lại nhánh per-type và nuke đúng 8 nhóm mà rào này sinh ra để cứu.
|
||||
// (`nextVersion` `:334-337` vốn đã tính per-Code ⇒ nhánh này làm 2 vế khớp nhau.)
|
||||
var actives = typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan
|
||||
// 🔴 [S182 R-6 · giữ nguyên hiệu lực @S187] Vị ngữ per-Code KHOÁ đúng 3 vế `Code == code
|
||||
// && ApplicableType == typeEnum && IsActive`. CẤM thay bằng biến-thể regex trên `Code`
|
||||
// (`^KHKK-N[1-8]$`, `^HD-...$`): mã nằm ngoài họ (vd `QT-KHKK-V2-001`, `QT-HD-V2-001`)
|
||||
// sẽ rơi lại nhánh per-type và nuke đúng tập mà rào này sinh ra để cứu. Phân loại theo
|
||||
// ApplicableType, KHÔNG theo hình dạng mã. (`nextVersion` phía trên vốn đã tính
|
||||
// per-Code toàn cục ⇒ nhánh này làm 2 vế khớp nhau.)
|
||||
var actives = PerCodeDeactivationTypes.Contains(typeEnum)
|
||||
? await db.ApprovalWorkflows
|
||||
.Where(w => w.Code == code && w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct)
|
||||
|
||||
@ -1530,4 +1530,299 @@ public class UpdateAwDefinitionTests
|
||||
"D5 chỉ áp cho họ KHKK-N[1-8]; chặn cả 'bản duy nhất' của quy trình chung là siết oan");
|
||||
}
|
||||
}
|
||||
|
||||
// ###################################################################
|
||||
// [S187 W1 — YC-023, 2026-08-10] Cùng HAI quả bom, nay cho `ApplicableType = Contract` (3).
|
||||
// SPEC: `.claude/workflows/runs/2026-08-10-S187-duyet-hd-tu-khkk/spec-duyet-hd-10-08-2026.md`
|
||||
// MỤC 2 W1 (+ `sub-plan-review-fable.md` §6 FLAG-5, §7 hàng W1).
|
||||
//
|
||||
// 4 lưới dưới đây viết TRƯỚC khi BE land ⇒ ca nào ĐỎ hôm nay là ĐỎ HỢP LỆ; hình dạng
|
||||
// "đỏ vì chưa làm" ghi trong `sub-test-specialist-0.md §4`.
|
||||
//
|
||||
// 🔴 VÌ SAO type-3 lặp lại đúng vết type-10: W3 sắp seed 7 quy trình `HD-*` SỐNG SONG SONG
|
||||
// cùng `ApplicableType = 3` (mỗi loại HĐ một Code). Kể từ lúc đó, câu tự khai ở
|
||||
// `ApprovalWorkflowV2AdminFeatures.cs:363` — "các loại khác: per-TYPE như cũ, 1 active/type
|
||||
// là bất biến của chúng" — thành SAI với type-3, và:
|
||||
// • bom-1 `:370-377`: ternary chỉ mở nhánh per-Code cho type-10 ⇒ một cú "Tạo phiên bản
|
||||
// mới" trên panel loại HĐ X tắt IsActive của 6 loại còn lại, im lặng, không endpoint
|
||||
// bật lại (đúng hình vụ nổ mà T1 đã tả cho KHKK).
|
||||
// • bom-2 `:205-215`: `versions.FirstOrDefault(v => v.IsActive)` mỗi type ⇒ 7 active
|
||||
// cùng type-3 chỉ hiện 1. "Giấu 6" là THẬT chứ không phải thẩm mỹ: FE render thẻ đang
|
||||
// áp dụng từ ô `active` (`ApprovalWorkflowsV2Page.tsx:381-389`) còn danh sách lịch sử
|
||||
// thì lọc `history.filter(d => !d.isActive)` (`:485-492`) ⇒ 6 bản active còn lại
|
||||
// KHÔNG xuất hiện ở đâu trên màn hình.
|
||||
//
|
||||
// ⚠️ R-6 (`:365-368`) CẤM biến-thể regex `^HD-...$`: mã type-3 ngoài họ vẫn phải per-Code —
|
||||
// đó là chân (a) của ca thứ hai.
|
||||
// 🔸 VÙNG KHÔNG PHỦ ĐƯỢC (khai, không giả vờ phủ): vế `ApplicableType == typeEnum` trong nhánh
|
||||
// per-Code không dựng được ca phân biệt — cần 2 workflow CÙNG Code KHÁC type, mà
|
||||
// `ApprovalWorkflowConfiguration.cs:23` `HasIndex(Code, Version).IsUnique()` là UNIQUE TOÀN
|
||||
// CỤC (không kèm type) và `nextVersion` `:351-353` cũng tính per-Code toàn cục.
|
||||
// 🔸 KHÔNG viết lại ca "type-1 tạo mã mới vẫn tắt mã cũ cùng type": T2 (`:1152`) đã phủ y hệt
|
||||
// — nó là rào chống bản vá "nới per-Code cho MỌI type", nhân bản cho type-3 không thêm sức
|
||||
// phân biệt nào.
|
||||
// ###################################################################
|
||||
|
||||
private const ApprovalWorkflowApplicableType Hd = ApprovalWorkflowApplicableType.Contract;
|
||||
|
||||
/// 7 loại HĐ = 7 Code type-3 sống song song, v01 IsActive — hình dạng prod SAU seeder W3.
|
||||
/// 🔴 Chuỗi mã ở đây chỉ là DỮ LIỆU của ca đo (W3 mới chốt mã thật, FLAG-6 ASCII UPPER):
|
||||
/// thứ đang đo là "phạm vi tắt = theo CODE trong type-3", không phải một chuỗi cụ thể.
|
||||
/// Viết HOA sẵn vì handler chuẩn hoá `ToUpperInvariant` (`:348`) còn SQLite so `=` kiểu
|
||||
/// BINARY (ngược prod `*_CI_AS`) — seed thường sẽ đo nhầm cơ chế.
|
||||
private static readonly string[] ContractFamilyCodes =
|
||||
["HD-TP", "HD-GK", "HD-NCC", "HD-DV", "HD-MB", "HD-NT-NCC", "HD-NT-DV"];
|
||||
|
||||
private static async Task SeedSevenContractWorkflowsAsync(TestApplicationDbContext db, Guid approverId)
|
||||
{
|
||||
foreach (var code in ContractFamilyCodes)
|
||||
await SeedWorkflowAsync(db, Hd, [[(1, approverId)]], code: code);
|
||||
}
|
||||
|
||||
/// 🔴 ĐƠN VỊ CỦA SỰ CỐ THẬT (mirror `KhkkGroupsWithActiveVersionAsync`): "bao nhiêu LOẠI HĐ
|
||||
/// còn quy trình để pin". Loại mất bản active là loại không lập được phiếu duyệt nữa.
|
||||
private static async Task<int> ContractCodesWithActiveVersionAsync(TestApplicationDbContext db)
|
||||
{
|
||||
var rows = await ReadWorkflowsAsync(db, Hd);
|
||||
return rows.Where(r => r.IsActive).Select(r => r.Code).Distinct().Count();
|
||||
}
|
||||
|
||||
/// Handler overview hỏi `UserManager` 3 nhóm quyền (`ApprovalWorkflowV2AdminFeatures.cs:166-168`)
|
||||
/// để bật cờ hiển thị CanEditPro/CanEditCcmBudget. Role chưa có trong store là chuyện HẠ TẦNG
|
||||
/// test chứ không phải hành vi đang đo ⇒ tạo sẵn cho khỏi lẫn nguyên nhân ĐỎ.
|
||||
private static async Task EnsureBudgetEditorRolesAsync(Harness h)
|
||||
{
|
||||
var rm = h.Fix.Services.GetRequiredService<RoleManager<Role>>();
|
||||
foreach (var role in new[] { AppRoles.Admin, AppRoles.Procurement, AppRoles.CostControl })
|
||||
{
|
||||
if (!await rm.RoleExistsAsync(role))
|
||||
await rm.CreateAsync(new Role { Id = Guid.NewGuid(), Name = role });
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔴 "KÊNH ĐƯƠNG NHIỆM" của một mục overview = MỌI ngả của `AwTypeSummaryDto` trỏ tới
|
||||
/// `AwDefinitionDto` (đơn hoặc tập hợp), **TRỪ `History`**.
|
||||
///
|
||||
/// Trừ `History` vì nó là KHO PHIÊN BẢN — chứa cả bản đã tắt — và FE render nó bằng
|
||||
/// `history.filter(d => !d.isActive)` (`ApprovalWorkflowsV2Page.tsx:485-492`) ⇒ một quy trình
|
||||
/// ĐANG ACTIVE mà chỉ nằm trong `History` thì không lên màn hình ở đâu cả. Nói cách khác:
|
||||
/// có mặt trong `History` KHÔNG phải là "được trả về như bản đang áp dụng".
|
||||
///
|
||||
/// Đọc bằng reflection để ca đo KHÔNG ép hình dạng bản vá: nó XANH với cả (a) thêm một trường
|
||||
/// tập hợp mới cạnh `Active`, lẫn (b) tách mỗi Code thành một mục riêng cùng `applicableType`.
|
||||
/// Hôm nay reflection chỉ thấy đúng ô `Active` đơn ⇒ ĐỎ vì HÀNH VI, không phải vì biên dịch.
|
||||
private static IEnumerable<AwDefinitionDto> SurfacedAsCurrent(AwTypeSummaryDto summary)
|
||||
{
|
||||
foreach (var prop in typeof(AwTypeSummaryDto).GetProperties())
|
||||
{
|
||||
if (prop.Name == nameof(AwTypeSummaryDto.History)) continue;
|
||||
switch (prop.GetValue(summary))
|
||||
{
|
||||
case AwDefinitionDto one:
|
||||
yield return one;
|
||||
break;
|
||||
case IEnumerable<AwDefinitionDto> many:
|
||||
foreach (var def in many) yield return def;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<AwDefinitionDto> ActiveChannel(
|
||||
AwAdminOverviewDto overview, ApprovalWorkflowApplicableType type)
|
||||
=> overview.Types.Where(t => t.ApplicableType == (int)type)
|
||||
.SelectMany(SurfacedAsCurrent).ToList();
|
||||
|
||||
private static List<AwDefinitionDto> HistoryChannel(
|
||||
AwAdminOverviewDto overview, ApprovalWorkflowApplicableType type)
|
||||
=> overview.Types.Where(t => t.ApplicableType == (int)type)
|
||||
.SelectMany(t => t.History).ToList();
|
||||
|
||||
// ===================================================================
|
||||
// (W1a-1) 🔴 BOM ĐANG ARMED cho type-3 — tạo v02 cho MỘT loại HĐ không được tắt 6 loại kia.
|
||||
//
|
||||
// Đo bằng đơn vị của sự cố: số LOẠI HĐ còn quy trình active. Hôm nay `:374-376` tắt theo TYPE
|
||||
// ⇒ sau một cú bấm "Tạo phiên bản mới" trên panel HĐ Mua bán, con số đó tụt 7 → 1.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task W1a_CreateNewVersionForOneContractCode_DeactivatesOnlyThatCode_SixOtherContractTypesStayActive()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await SeedSevenContractWorkflowsAsync(h.Db, alice.Id);
|
||||
|
||||
// Tiền đề đo: 7 loại, TẤT CẢ đang có bản active. Thiếu chốt này thì một seed hỏng
|
||||
// (vd 1 loại đã tắt sẵn) vẫn cho assert phía dưới xanh ở thế giới sai.
|
||||
(await ContractCodesWithActiveVersionAsync(h.Db)).Should().Be(7,
|
||||
"tiền đề: seeder W3 để cả 7 loại HĐ có quy trình active");
|
||||
|
||||
await new CreateAwDefinitionCommandHandler(h.Db).Handle(
|
||||
NewVersionCommand(Hd, "HD-MB", alice.Id), CancellationToken.None);
|
||||
|
||||
var rows = await ReadWorkflowsAsync(h.Db, Hd);
|
||||
rows.Should().HaveCount(8, "POST đẻ ĐÚNG 1 dòng mới (v02 của HD-MB) — không xoá/không thêm dòng nào khác");
|
||||
|
||||
rows.Single(r => r is { Code: "HD-MB", Version: 1 }).IsActive.Should().BeFalse(
|
||||
"cùng Code thì bản cũ PHẢI nhường chỗ — vế 'ratchet một chiều' của chính loại Mua bán");
|
||||
rows.Single(r => r is { Code: "HD-MB", Version: 2 }).IsActive.Should().BeTrue(
|
||||
"version vừa tạo là bản đang dùng của loại Mua bán");
|
||||
|
||||
rows.Where(r => r.Code != "HD-MB").Should().OnlyContain(r => r.IsActive,
|
||||
"🔴 tập bị tắt phải lọc theo CODE trong type-3, không theo TYPE. Lọc theo type thì một cú "
|
||||
+ "bấm trên panel Mua bán tắt luôn 6 loại HĐ còn lại — và KHÔNG có endpoint bật lại");
|
||||
|
||||
(await ContractCodesWithActiveVersionAsync(h.Db)).Should().Be(7,
|
||||
"số LOẠI HĐ còn quy trình để pin phải BẤT BIẾN qua thao tác của một loại khác; tụt xuống 1 "
|
||||
+ "là đúng hình vụ nổ đã xảy ra với 8 nhóm KHKK (T1)");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (W1a-2) R-6 — mã type-3 NGOÀI họ `HD-*` (vd quy trình HĐ chung/legacy). Hai chân, mỗi chân
|
||||
// giết một bản vá sai:
|
||||
// (a) tạo `QT-HD-V2-001` ⇒ 7 loại BẤT BIẾN → giết bản vá regex `^HD-...$` (mã ngoài họ rơi
|
||||
// lại nhánh per-type và NUKE đúng 7 loại mà rào này sinh ra để cứu — R-6 CẤM);
|
||||
// (b) tạo LẠI CHÍNH mã đó ⇒ v01 phải tắt → giết bản vá lười "type-3 thì đừng tắt gì cả"
|
||||
// (chân (a) một mình sẽ XANH với bản vá đó).
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task W1a_CreateType3_WithOutOfFamilyCode_LeavesSevenContractCodesActive_AndStillRatchetsItsOwnCode()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await SeedSevenContractWorkflowsAsync(h.Db, alice.Id);
|
||||
var handler = new CreateAwDefinitionCommandHandler(h.Db);
|
||||
|
||||
// ---- (a) mã type-3 chưa từng có ⇒ KHÔNG có bản cũ nào để tắt.
|
||||
await handler.Handle(NewVersionCommand(Hd, "QT-HD-V2-001", alice.Id), CancellationToken.None);
|
||||
|
||||
(await ContractCodesWithActiveVersionAsync(h.Db)).Should().Be(8,
|
||||
"🔴 R-6: quy trình type-3 mã ngoài họ (chung/legacy) tạo mới KHÔNG được đụng 7 loại HĐ. "
|
||||
+ "Công thức regex cho mã này rơi về nhánh per-type ⇒ NUKE cả 7");
|
||||
|
||||
var afterA = await ReadWorkflowsAsync(h.Db, Hd);
|
||||
afterA.Single(r => r.Code == "QT-HD-V2-001").IsActive.Should().BeTrue();
|
||||
afterA.Where(r => ContractFamilyCodes.Contains(r.Code)).Should().OnlyContain(r => r.IsActive,
|
||||
"7 loại + 1 quy trình chung, cùng sống song song");
|
||||
|
||||
// ---- (b) CHÍNH mã đó, lần thứ hai ⇒ ratchet per-Code vẫn phải chạy.
|
||||
await handler.Handle(NewVersionCommand(Hd, "QT-HD-V2-001", alice.Id), CancellationToken.None);
|
||||
|
||||
var afterB = await ReadWorkflowsAsync(h.Db, Hd);
|
||||
afterB.Single(r => r is { Code: "QT-HD-V2-001", Version: 1 }).IsActive.Should().BeFalse(
|
||||
"per-Code áp cho MỌI mã type-3, kể cả ngoài họ — 'type-3 thì không tắt gì' là bản vá làm "
|
||||
+ "2 version cùng mã cùng active, phía pin quy trình bốc ngẫu nhiên");
|
||||
afterB.Single(r => r is { Code: "QT-HD-V2-001", Version: 2 }).IsActive.Should().BeTrue();
|
||||
(await ContractCodesWithActiveVersionAsync(h.Db)).Should().Be(8, "8 mã vẫn bất biến sau lượt 2");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (W1b-1) 🔴 BOM-2 — overview type-3 phải trả ĐỦ mọi quy trình đang áp dụng của loại đó.
|
||||
// Hôm nay `:205-215` chọn `FirstOrDefault(v => v.IsActive)` ⇒ trả về 1, 6 cái còn lại chỉ
|
||||
// nằm trong `History` (nơi FE lọc `!isActive` để dựng danh sách lịch sử ⇒ biến mất khỏi UI).
|
||||
//
|
||||
// Đo CẢ HAI nhánh `typesToReturn` (`:201-203`): có lọc `applicableType` (đường deep-link
|
||||
// Designer, `ApprovalWorkflowsV2Page.tsx:311`) và không lọc (lưới landing `:378`) — bản vá
|
||||
// chỉ đụng một nhánh sẽ ĐỎ ở nhánh kia.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task W1b_OverviewForContractType_SurfacesEveryActiveWorkflowOfThatType_NotJustTheFirst()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await EnsureBudgetEditorRolesAsync(h);
|
||||
await SeedSevenContractWorkflowsAsync(h.Db, alice.Id);
|
||||
|
||||
// HD-MB đã qua một lần "Tạo phiên bản mới": v01 tắt, v02 đang áp dụng. Có cặp này thì
|
||||
// ca đo phân biệt được "trả về BẢN ĐANG ÁP DỤNG" với "trả về bừa một bản của Code đó".
|
||||
await SeedWorkflowAsync(h.Db, Hd, [[(1, alice.Id)]], code: "HD-MB", version: 2);
|
||||
var mbV1 = await h.Db.ApprovalWorkflows
|
||||
.FirstAsync(w => w.Code == "HD-MB" && w.Version == 1, CancellationToken.None);
|
||||
mbV1.IsActive = false;
|
||||
await h.Db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
(await ContractCodesWithActiveVersionAsync(h.Db)).Should().Be(7,
|
||||
"tiền đề: đúng 7 loại HĐ đang có bản áp dụng (HD-MB đang ở v02)");
|
||||
|
||||
var handler = new GetAwAdminOverviewQueryHandler(h.Db, h.Um);
|
||||
|
||||
// ---- Nhánh 1: lọc theo type (đường Designer per-type mở từ deep-link).
|
||||
var filtered = await handler.Handle(
|
||||
new GetAwAdminOverviewQuery(ApplicableType: (int)Hd), CancellationToken.None);
|
||||
var filteredChannel = ActiveChannel(filtered, Hd);
|
||||
|
||||
filteredChannel.Select(d => d.Code).Should().BeEquivalentTo(ContractFamilyCodes,
|
||||
"🔴 overview phải trả ĐỦ mọi quy trình đang áp dụng của type-3. Trả 1 thì 6 loại HĐ còn "
|
||||
+ "lại không có thẻ 'đang áp dụng' nào, mà danh sách lịch sử thì lọc `!isActive` ⇒ chúng "
|
||||
+ "biến mất khỏi màn hình Designer, im lặng");
|
||||
filteredChannel.Should().OnlyContain(d => d.IsActive,
|
||||
"kênh đương nhiệm chỉ được chứa bản ĐANG áp dụng — nhét cả bản đã tắt vào là đổi nghĩa ô này");
|
||||
filteredChannel.Single(d => d.Code == "HD-MB").Version.Should().Be(2,
|
||||
"với loại đã có 2 version, phải trả bản v02 đang áp dụng chứ không phải bản đầu tiên gặp");
|
||||
|
||||
HistoryChannel(filtered, Hd).Select(d => (d.Code, d.Version)).Distinct().Should().HaveCount(8,
|
||||
"kho phiên bản KHÔNG được teo lại: 7 loại + 1 bản HD-MB v01 đã tắt — màn 'phiên bản cũ' "
|
||||
+ "sống bằng đúng danh sách này");
|
||||
|
||||
// ---- Nhánh 2: không lọc (lưới landing map qua mọi ApplicableType).
|
||||
var all = await handler.Handle(new GetAwAdminOverviewQuery(), CancellationToken.None);
|
||||
ActiveChannel(all, Hd).Select(d => d.Code).Should().BeEquivalentTo(ContractFamilyCodes,
|
||||
"hai nhánh `typesToReturn` (:201-203) đi hai đường khác nhau — vá một nhánh là để lọt nhánh kia");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (W1b-2) HỒI QUY cho các loại 1-active-per-type: bản vá W1b chỉ được đổi hành vi ở chỗ có
|
||||
// NHIỀU bản cùng áp dụng. Type-1 (Duyệt NCC) vẫn phải chỉ ra ĐÚNG MỘT bản đang áp dụng.
|
||||
//
|
||||
// Ca này XANH hôm nay và phải XANH sau bản vá — nó giết bản vá lười "đổ nguyên `History` vào
|
||||
// kênh đương nhiệm" (type-1 sẽ hiện cả v01 đã tắt) và bản vá "gộp mọi type vào một rổ".
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task W1b_OverviewForSingleActiveType_KeepsExactlyOneCurrentDesignation_AndDoesNotLeakOtherTypes()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await EnsureBudgetEditorRolesAsync(h);
|
||||
|
||||
await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
[[(1, alice.Id)]], code: "QT-DN-V2-001", version: 1);
|
||||
await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
[[(1, alice.Id)]], code: "QT-DN-V2-001", version: 2);
|
||||
var dnV1 = await h.Db.ApprovalWorkflows
|
||||
.FirstAsync(w => w.Code == "QT-DN-V2-001" && w.Version == 1, CancellationToken.None);
|
||||
dnV1.IsActive = false;
|
||||
await h.Db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// Chứng nhân rò rỉ: 2 type khác cùng tồn tại trong DB, mỗi type có bản active riêng.
|
||||
await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
|
||||
[[(1, alice.Id)]], code: "QT-DNPA-V2-001");
|
||||
await SeedSevenContractWorkflowsAsync(h.Db, alice.Id);
|
||||
|
||||
var all = await new GetAwAdminOverviewQueryHandler(h.Db, h.Um)
|
||||
.Handle(new GetAwAdminOverviewQuery(), CancellationToken.None);
|
||||
var channel = ActiveChannel(all, ApprovalWorkflowApplicableType.DuyetNcc);
|
||||
|
||||
channel.Should().ContainSingle(
|
||||
"type-1 GIỮ ngữ nghĩa 1-active-per-type — nới kênh đương nhiệm cho MỌI type sẽ hiện "
|
||||
+ "cả bản đã tắt, và trang Duyệt NCC có 2 thẻ 'đang áp dụng'");
|
||||
channel.Single().Code.Should().Be("QT-DN-V2-001");
|
||||
channel.Single().Version.Should().Be(2, "bản đang áp dụng là v02, không phải v01 đã tắt");
|
||||
channel.Should().OnlyContain(d => d.ApplicableType == (int)ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
"mục của type-1 không được lẫn quy trình của type khác");
|
||||
|
||||
HistoryChannel(all, ApprovalWorkflowApplicableType.DuyetNcc)
|
||||
.Select(d => d.Version).Should().BeEquivalentTo(new[] { 1, 2 },
|
||||
"kho phiên bản của type-1 vẫn đủ 2 bản");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user