# sub-implementer-backend-w1green — W1-GREEN: gỡ 2 bom per-type cho type-3 (Contract)

> Run `2026-08-10-S187-duyet-hd-tu-khkk` · vai `implementer-backend` · 2026-08-10.
> Ghi TỪNG MỤC trong lúc làm (chống #53). Mục chưa chạy = tiêu đề + "CHƯA ĐO", cấm điền số trước.

## §0 — Ground-truth ĐO TƯƠI trước khi gõ

| Thứ đo | Lệnh | Kết quả |
|---|---|---|
| HEAD | `git log --oneline -1` | `cc2dae26 wal: flush 20260810T1746` |
| Cây làm việc | `git status --porcelain` | 9 file `M`: `ContractFeatures.cs` · `ContractDtos.cs` · `fe-{admin,user}/types/contracts.ts` · `fe-user/.../MyContractsPage.tsx` (= **lane W2/W7 song song**) + `UpdateAwDefinitionTests.cs` (= **lane test-specialist đã land**) + 3 doc/governance. **0 file `Persistence/Migrations/`** · file đích của tôi (`ApprovalWorkflowV2AdminFeatures.cs`) **CHƯA ai đụng** ⇒ mọi thay đổi trong đó dưới đây là của tôi |
| Site deactivate | `grep -n IsActive` file đích | **DUY NHẤT 1 site ghi `IsActive = false`** — `:377` trong `CreateAwDefinitionCommandHandler`. `UpdateAwDefinitionCommandHandler` cố ý GIỮ `IsActive` (`:448` "Giữ Id là TOÀN BỘ mục đích") ⇒ **không có site anh em phải vá kèm** (kỷ luật grep-cùng-lớp, S122) |
| Nhánh per-Code hiện có | `grep -n "typeEnum == ApprovalWorkflowApplicableType"` toàn `src/Backend` | **1 hit** `:370` — không có bản sao luật ở nơi khác |
| Consumer DTO | `grep -n AwTypeSummaryDto` toàn kho | BE: khai `:82` + dựng `:209` (**1 site dựng duy nhất**) · Api chỉ trả `ActionResult<AwAdminOverviewDto>` · `tests/**` **0 site `new AwTypeSummaryDto`** ⇒ thêm tham số vào record positional KHÔNG vỡ biên dịch chỗ nào |

## §1 — Đọc 4 assert quyết định HÌNH DẠNG bản vá (trước khi chọn hình)

`SurfacedAsCurrent` (`UpdateAwDefinitionTests.cs:1614-1629`) duyệt **MỌI** property của `AwTypeSummaryDto` trừ `History`, yield cả `AwDefinitionDto` đơn lẫn `IEnumerable<AwDefinitionDto>` ⇒ kênh đương-nhiệm = **hợp của mọi ngả ≠ History**, và:

- ca 3 `:1761` `BeEquivalentTo(ContractFamilyCodes)` = song ánh 7↔7 ⇒ **trùng lặp cũng ĐỎ y như thiếu**;
- ca 4 `:1815` `ContainSingle()` cho type-1 ⇒ type 1-active **không được đôi**.

🔴 Hệ quả: hình (a) hiểu ngây thơ — giữ `Active` = bản đầu **và** thêm `ActiveWorkflows` = **MỌI** bản active — sẽ **ĐỎ CẢ 2 CA** (type-3: 8 mục, `HD-*` đầu bảng bị đếm 2 lần; type-1: 2 mục). Đề bài đã cảnh báo đúng chỗ này ("không leak/không đôi"). Hai cách thoát:

| | Hình | Ca 3 | Ca 4 | Rủi ro FE (ngoài lãnh địa tôi) |
|---|---|---|---|---|
| B1 | `Active = null` khi type có >1 active, tập hợp gánh TẤT | ✔ | ✔ | 🔴 **ĐỔI HÀNH VI ĐANG CHẠY**: `ApprovalWorkflowsV2Page.tsx:381-389` + `:455-479` đọc thẳng `t.active`; type-10 KHKK **hôm nay đã có 8 active** ⇒ thẻ landing + panel của nó mất nội dung ngay lượt deploy |
| **B2 (CHỌN)** | `Active` **y nguyên công thức cũ** + trường mới gánh **phần CÒN LẠI** | ✔ | ✔ | **0** — `active` giữ đúng giá trị cũ từng byte (`versions.FirstOrDefault(v => v.IsActive)` ≡ `actives.FirstOrDefault()`), bản vá thuần **cộng thêm** |

⇒ Chọn **B2**, tên trường `ActiveOthers`, hợp đồng ghi thẳng vào doc-comment: **tập đầy đủ = `[Active]` ∪ `ActiveOthers`, KHÔNG lặp**. Tên `ActiveWorkflows` bị loại vì nó *hứa* "tất cả" trong khi ruột là "phần còn lại" — tên sai nghĩa là bug của lượt sau.

🔸 Vá bom-2 cố ý **KHÔNG hỏi type**: nó chỉ đếm "có mấy bản đang áp dụng thì trả bấy nhiêu" ⇒ tự đúng cho mọi loại family tương lai, và ca 4 xanh vì type-1 **thật sự** có 1 active (không phải vì tôi khai riêng cho nó).

## §2 — 4 mảnh đã sửa, 1 FILE DUY NHẤT

`src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` — `git diff --stat` = **+63 / −14, 1 file**.

| # | Dòng (SAU sửa) | Sửa gì |
|---|---|---|
| M1 | `:372-380` | **MỚI** `private static readonly ApprovalWorkflowApplicableType[] PerCodeDeactivationTypes = [ContractSigningPlan, Contract]` ở đầu `CreateAwDefinitionCommandHandler` + tiêu chí vào-danh-sách (loại có N quy trình sống song song cùng type, mỗi cái một `Code`) |
| M2 | `:422` | **bom-1**: `typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan` → `PerCodeDeactivationTypes.Contains(typeEnum)`. Hai nhánh truy vấn GIỮ NGUYÊN từng vế (3 vế per-Code · 2 vế per-TYPE) — R-6 tôn trọng: phân loại theo **TYPE**, 0 regex/pattern trên `Code` |
| M3 | `:395-420` (comment cũ `:363`) | Câu tự-khai "các loại khác: per-TYPE như cũ — 1 active/type là bất biến" **SAI cho type-3 sau W3** ⇒ viết lại neo vào TẬP TYPE tường minh, kèm ghi vết *vì sao câu cũ hỏng* (tập per-Code nở thêm thì câu neo-theo-con-số hỏng lần nữa) + R-6 mở rộng ví dụ `^HD-...$` |
| M4 | `:85-101` (DTO) + `:228-245` (handler) | **bom-2**: `AwTypeSummaryDto` **+`List<AwDefinitionDto> ActiveOthers`** giữa `Active` và `History`; handler tính `actives = versions.Where(IsActive)` rồi trả `Active = actives.FirstOrDefault()` + `ActiveOthers = actives.Skip(1)`. `Active` **≡ giá trị cũ từng byte** (`versions.FirstOrDefault(v => v.IsActive)`) ⇒ 0 rủi ro cho `ApprovalWorkflowsV2Page.tsx:381-389/455-479` đang đọc `t.active` |

🔸 Ghi thêm vào doc-comment `Active` (`:88-92`): với loại family nó chỉ là **MỘT trong N**, panel per-Code phải tự cắt theo `Code` như KHKK đang làm (`:321-339`) — để lane FE không đọc nhầm ô này lần nữa.
🔸 **KHÔNG có site anh em bị bỏ sót**: `IsActive = false` chỉ ghi ở đúng 1 nơi (`:429`); `UpdateAwDefinitionCommandHandler` cố ý giữ `IsActive` (`:500`).

## §3 — Chạy (số ĐO SAU byte cuối cùng, không phải số giữa chừng)

`dotnet test tests/SolutionErp.Infrastructure.Tests --filter "FullyQualifiedName~UpdateAwDefinitionTests"`:

```
Passed!  - Failed:     0, Passed:    31, Skipped:     0, Total:    31, Duration: 27 s
```

Đích danh 4 ca W1 (`--filter ...UpdateAwDefinitionTests.W1 --logger console;verbosity=normal`):

```
Passed ...W1a_CreateType3_WithOutOfFamilyCode_LeavesSevenContractCodesActive_AndStillRatchetsItsOwnCode [3 s]
Passed ...W1a_CreateNewVersionForOneContractCode_DeactivatesOnlyThatCode_SixOtherContractTypesStayActive [1 s]
Passed ...W1b_OverviewForContractType_SurfacesEveryActiveWorkflowOfThatType_NotJustTheFirst [1 s]
Passed ...W1b_OverviewForSingleActiveType_KeepsExactlyOneCurrentDesignation_AndDoesNotLeakOtherTypes [504 ms]
```

⇒ 3 ĐỎ → XANH · **control ca 4 GIỮ xanh** (không leak/không đôi) · 27 ca cũ giữ xanh (31 − 4).

**Chuỗi nhân-quả "xanh này là do bản vá", không phải do đo lệch:** (1) test-specialist ghi RED 3-ca đích danh trên `3a311c04` (`sub-test-specialist-0.md §4`); (2) §0 của tôi đo `git status` — file đích **chưa ai chạm** giữa 2 mốc; (3) tôi chỉ sửa đúng file đó; (4) chạy lại xanh. 🔸 KHÔNG dùng `git stash` để đối chứng vì cây làm việc đang có 3 lane khác chạy song song — stash là thao tác toàn-cây, rủi ro nuốt việc người khác lớn hơn giá trị của phép đối chứng.

**Hồi quy hàng xóm** (`ContractSigningPlanApprovalTests` + `ContractV2PipelineTests` + `ContractWorkflowServiceApproveV2Tests` + `ContractV2SchemaPersistenceTests` + `CreateContractFromSigningPlanTests`): `Failed: 0, Passed: 51`. Đây là tập lớp có đụng `CreateAwDefinitionCommandHandler` (type-10) hoặc seed workflow type-3 — tức đúng vùng bản vá có thể văng miểng.
🔸 **KHÔNG chạy full-suite**: lane W2/W7 đang sửa `ContractFeatures.cs`/FE giữa chừng ⇒ số full-suite lúc này là snapshot dở (gotcha #68) và không phải số của tôi. Phán quyết full-suite = LEAD chạy sau khi mọi lane dừng tay.

## §4 — Build + kiểm scope

- `dotnet build SolutionErp.slnx` → **Build succeeded, 0 Warning(s), 0 Error(s)** (4.45s), chạy SAU byte cuối.
- `git status --porcelain -- src/Backend/SolutionErp.Infrastructure/Persistence/Migrations` → **RỖNG** ⇒ 0-mig như yêu cầu.
- File `src/**` duy nhất tôi đụng: `ApprovalWorkflowV2AdminFeatures.cs`. **0 chạm** `Contracts/ContractFeatures.cs` · `Contracts/Dtos/ContractDtos.cs` · `tests/**` · `fe-*/**` (các file đó `M` trong `git status` là của lane W2/W7/FE).
- Không commit (em main commits).

== HẾT ==
