# sub-investigator-codebase-1 — LENS TAXONOMY (YC-032)

> Lăng-kính: tách `01. HĐ Thầu phụ` thành 2 menu thì TỐN GÌ, và tách Ở TẦNG NÀO.
> Kỷ luật: mọi số từ lệnh vừa chạy, kèm lệnh. 0-hit kèm control-dương.

## 0. Khung (ghi lượt 1)

- [ ] A. Hai phương án tách + đếm consumer
- [ ] B. Enum persist + migration
- [ ] C. 7 quy trình HD-* + guard
- [ ] D. Rủi ro / điều CHƯA đo được

## A. Hai phương án tách + đếm consumer

### A.0 — Mẫu số: `ContractType` có bao nhiêu consumer (đo, kèm lệnh)

```
rg -l "ContractType" src fe-admin/src fe-user/src --glob '!**/node_modules/**'                       => 133 file
rg -l "ContractType" ... --glob '!**/Persistence/Migrations/**'                                       =>  59 file  (mã SỐNG)
rg -l "ContractType" src/Backend/SolutionErp.Infrastructure/Persistence/Migrations                    =>  74 file  (Designer/Snapshot auto-gen, KHÔNG sửa tay)
rg -c "ContractType" tests                                                                            =>  14 file
rg -c "HopDongThauPhu" --glob '!**/node_modules/**'                                                    =>  28 file (13 mã sống + 13 test/doc/runs)
```
⇒ Mẫu số THẬT phải sửa tay = **59 file mã sống**, KHÔNG phải 133. 74 file Designer là snapshot EF sinh lại.

### A.0-bis — 3 hằng-số CỐT nằm ở đâu (file PIN)

| Trục | File:line | Ruột |
|---|---|---|
| Enum loại HĐ | `src/Backend/SolutionErp.Domain/Contracts/ContractType.cs:3-12` | 7 member, `HopDongThauPhu = 1` |
| Họ quy trình duyệt V2 | `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42` | mảng 7 tuple `(Type, Code, ShortName)`; `CodeFor` `:48-53`; `KeyToken` `:59-62` |
| Policy 9-phase V1 | `src/Backend/SolutionErp.Domain/Contracts/WorkflowPolicy.cs:153-157` | `DefaultPolicyNameFor` — TP/GK/NCC → `"Standard"`, **`_ => "SkipCcm"`** (default-catch-all) |

🔴 **Phát hiện quan trọng #1 — `WorkflowPolicy.cs:153-157` KHÔNG cần sửa nếu A1 tách đúng:**
switch dùng `or` liệt 3 loại rồi `_ => SkipCcm`. Enum mới =8/=9 mà KHÔNG thêm vào vế `or` sẽ **rơi im lặng
vào SkipCcm** (mất phase CCM) — đây là *ghost-default*, không nổ, không cảnh báo. ⇒ A1 **BẮT BUỘC** thêm
2 member vào dòng `:155`, nếu quên thì lỗi câm.

### A.1 — PHƯƠNG ÁN A1: thêm 2 `ContractType` member (=8, =9)

Danh sách site liệt-đủ-7-loại (đo bằng `rg -n "HopDongNguyenTacDichVu" src fe-admin/src fe-user/src --glob '!**/Persistence/Migrations/**'` = 16 hit / 10 file — member cuối làm proxy "chỗ nào liệt đủ 7"):

| # | file:line | Việc phải làm khi +2 type |
|---|---|---|
| 1 | `Domain/Contracts/ContractType.cs:12` | +2 member (=8, =9) |
| 2 | `Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42` | +2 tuple ⇒ họ 7→**9** mã (kéo theo C) |
| 3 | `Domain/Contracts/WorkflowPolicy.cs:155` | +2 vào vế `or` (nếu không: ghost-default SkipCcm) |
| 4 | `Infrastructure/Services/ContractCodeGenerator.cs:27` | +2 nhánh prefix mã HĐ (RG-001) |
| 5 | `Infrastructure/Reports/ContractExcelExporter.cs:36` | +2 nhãn |
| 6 | `Application/Contracts/WorkflowAdminFeatures.cs:72` | +2 nhãn (Designer V1) |
| 7 | `Application/Contracts/ContractDetailsFeatures.cs:109,460` (+ `:263,:403` mẫu) | 🔴 dispatch **bảng chi tiết per-type** — xem A.1-bis |
| 8 | `Infrastructure/Persistence/DbInitializer.cs:1008,1157,1169,1247,1390` | seed WF V1 + demo data + menu `Ct_*` |
| 9 | `Domain/Identity/MenuKeys.cs` | sinh key `Ct_*` ×4 + `AwV2_Hd*` — xem A.1-ter |
| 10 | `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:59-65` | map `ct → HD-*` (7 entry) |
| 11 | `fe-admin/src/lib/contractWorkflowFamily.ts:46` | `CONTRACT_WF_FAMILY` 7 member |
| 12 | `fe-{user,admin}/src/components/Layout.tsx` | accordion 7 nhóm HĐ |
| 13 | `fe-{user,admin}/src/pages/contracts/ContractCreatePage.tsx` (7 hit mỗi bên) | dropdown loại |

### A.1-bis — 🔴 PHÁT HIỆN LỚN NHẤT: 7 BẢNG CHI TIẾT VẬT LÝ per-type (không phải TPH)

`ls src/Backend/SolutionErp.Domain/Contracts/Details/` = **8 file** (`ContractDetailBase.cs` + 7 subclass).
`ContractDetailBase.cs:6-11` khai nguyên văn: *"Mỗi loại HĐ có 1 bảng riêng (Option B theo user decision)…
**Không dùng EF TPH/TPT inheritance** — mỗi subclass tự khai báo entity ToTable riêng."*
`ContractDetailsConfiguration.cs` = 7 class config, 7 `ToTable(...)`: `ThauPhuDetails` `:14` · `GiaoKhoanDetails` `:38`
· `NhaCungCapDetails` `:64` · `DichVuDetails` `:92` · `MuaBanDetails` `:118` · `NguyenTacNccDetails` `:147` · `NguyenTacDvDetails` `:172`.

Dispatch 7-nhánh CỨNG (đo `rg -n "case ContractType" ContractDetailsFeatures.cs`):
- `ContractDetailsFeatures.cs:43,54,65,76,87,98,109` (nhánh đọc/GetDetails)
- `ContractDetailsFeatures.cs:436,440,444,448,452,456,460` (nhánh thứ 2)
- **21 handler** `EnsureContractType(…, ContractType.X, …)` — `:136,157,178,200,221,242,263` (Create) + `:289,307,326,345,364,384,403` (Update) + mẫu ở `:263/:403`.

⇒ **A1 sinh câu hỏi schema:** 2 loại Thầu phụ mới **dùng chung bảng `ThauPhuDetails`** (rẻ, 0 migration, nhưng
phá bất-biến "1 type = 1 bảng" đang có) **hay đẻ 2 bảng mới** (`ThauPhuVatTuDetails` / `ThauPhuThietBiDetails`
⇒ **+2 CreateTable, migration 74, bảng 97→99**, + ~6 handler CRUD ×2 + 2 DbSet + 2 nav-collection trên `Contract`).
🔴 Câu này **owner/lead phải chốt**, không phải suy được từ mã.

### A.1-ter — Menu/policy: `Ct_*` sinh động (RẺ) ⟂ `AwV2_Hd*` vào `All` (ĐẮT)

- `MenuKeys.cs:40-41` `ContractTypeCodes = ["ThauPhu","GiaoKhoan","NhaCungCap","DichVu","MuaBan","NguyenTacNcc","NguyenTacDv"]`
  + factory `:88-91` `Ct_{code}` / `_List` / `_Create` / `_Pending`.
  `MenuKeys.cs:69` khai rõ: *"`Ct_*` / `Pe_*` leaf **CỐ Ý NGOÀI `All`** — sinh động qua factory"* ⇒ **+2 typeCode
  = +8 menu-row, 0 policy mới**. Rẻ.
- `MenuKeys.cs:151` `ApprovalWorkflowContractLeaf(token) => "AwV2_Hd{token}"`, `:155-157` `ContractWorkflowMenuKeys`
  **sinh TỪ `ContractApprovalWorkflowFamily.Members`**, và `:275` `.. ContractWorkflowMenuKeys` **NẰM TRONG `All`**.
  ⇒ +2 member họ = **+2 menu-key vào `All` = +8 policy** (4 action × 2) và `MenuKeys.cs:147-148` cảnh báo cứng:
  *"Vào `All` thì **BẮT BUỘC** có menu-row trong `SeedMenuTreeAsync` — thiếu row = Permission FK **nổ ngay lần
  boot đầu** (invariant T6; test `ContractCatalogTests:445` bắt đúng ca này)."*
  ⇒ **by-construction**: sửa `ContractApprovalWorkflowFamily.Members` là menu-key + policy tự nở — KHÔNG mirror tay,
  nhưng PHẢI seed menu-row cùng lượt nếu không **boot nổ**.

### A.1-quater — Vòng seed menu + FE route: chuỗi phái sinh, **thêm 1 typeCode là chạy hết**

- Seed menu-row: `DbInitializer.cs:2192` `foreach (var code in MenuKeys.ContractTypeCodes)` ⇒ `:2198-2206`
  đẻ **1 group + 6 leaf** (`WfView/List/Create/Pending/Approved/Deleted`) + `:2197` thêm 1 leaf `Hdc_*` (Bảng cứng).
  Vòng thứ 2 `:2212`, vòng grant `:2578-2580`.
- Nhãn tiếng Việt **PHẢI thêm tay** (2 chỗ): `DbInitializer.cs:2054` (`["ThauPhu"]="01. HĐ Thầu phụ"`) và
  `:2375` (`Hdc_ThauPhu`) + `:2382-2388` (dict reconcile 7 nhãn `01.`→`07.`).
  🔴 Thêm 2 loại ⇒ **đánh số lại 01..09** hoặc chèn `01a/01b` — nhãn có SỐ THỨ TỰ là quyết định của owner
  (số này đã UPDATE prod theo UAT anh Kiệt @S159, `:2048-2049`).
- FE route: `fe-user/src/components/Layout.tsx:16-24` + `fe-admin/src/components/Layout.tsx:19` — dict
  `TYPE_CODE_TO_INT` **chép tay 7 entry mỗi bên** (+2 bản nữa ở `ApprovalWorkflowsV2Page.tsx:172` và
  `WorkflowsPage.tsx:77` ⇒ **4 bản sao trong FE**). `Layout.tsx:161-181` regex
  `^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$` → `TYPE_CODE_TO_INT[code]`; **`:165` `if (!typeInt) return null`**
  ⇒ thiếu 1 entry trong dict = **leaf DROP IM LẶNG khỏi sidebar (gotcha #50)** — đúng ca đã cháy @S190.

### A.2 — PHƯƠNG ÁN A2: giữ 1 `ContractType=1` + cờ/biến-thể (sub-type)

**Trạng thái hiện tại:** `Contract.cs` **KHÔNG có** trường biến-thể nào (đo `rg -n "public " Contract.cs` — 11 field
nghiệp vụ, gần nhất là `TemplateId` `:17` "Template dùng để render" và `BypassProcurementAndCCM` `:21`).
⇒ A2 phải **đẻ mới** 1 trong 2:
- (a) **cột enum mới** `ContractVariant?` trên `Contracts` ⇒ **1 AddColumn, Mig 74, 0 bảng mới** (rẻ hơn A1-có-bảng), hoặc
- (b) **tái dụng `Contract.TemplateId`** → `ContractTemplates` (`ContractTemplateConfiguration.cs:11,16,24`:
  `ContractType` nullable + có index; `FormFeatures.cs:33` filter `x.ContractType == request.ContractType`)
  ⇒ **0 cột mới, 0 migration** — 2 form = 2 row `ContractTemplates` cùng `ContractType=1`.

**Nhưng chi phí A2 KHÔNG rẻ hơn ở tầng TAXONOMY**, vì mọi trục phái sinh đang khoá bằng `ContractType`:

| Trục | Hôm nay khoá bằng | A2 phải làm gì |
|---|---|---|
| Menu key + route | `MenuKeys.ContractTypeCodes` → `TYPE_CODE_TO_INT[code]` → `?type=N` | thêm 2 pseudo-code **không có** int ⇒ `Layout.tsx:165` trả `null` ⇒ **mất leaf**; phải nới regex + đẻ param mới (`?variant=`) ở **2 Layout + trang List + trang Create + Hdc** |
| Quy trình duyệt | `ContractApprovalWorkflowFamily.CodeFor(ContractType)` | 2 biến-thể **buộc dùng chung `HD-TP`** — muốn khác lộ trình duyệt thì phải đổi **chữ ký** `CodeFor`/guard (xem C) |
| Bảng chi tiết | 7 bảng vật lý per-type | dùng chung `ThauPhuDetails` (hợp lý) |
| Mã HĐ RG-001 | `ContractCodeGenerator.cs:27` switch theo type | 2 biến-thể chung prefix `HĐTP` — **có thể là điều owner muốn**, hoặc không |
| Form template | `ContractTemplate.ContractType` (nullable int) | **KHÔNG phân biệt được 2 biến-thể** nếu chỉ có `ContractType=1` ⇒ chọn form phải đi qua `TemplateId`/`FormCode`, không qua type |

**Đếm thô:** A1 ≈ **13 site mã sống** (bảng A.1) + 4 bản `TYPE_CODE_TO_INT` — nhưng **~6/13 là by-construction**
(sửa 1 mảng nguồn là nở hết: `ContractTypeCodes`, `ContractApprovalWorkflowFamily.Members` → menu-key/policy/seed-loop).
A2 ≈ **1 cột (hoặc 0)** + **~8-10 site ĐẺ TRỤC MỚI** (`?variant=` xuyên 2 FE app + BE filter + guard) —
tức A2 **ít sửa chỗ cũ hơn nhưng đẻ một khái niệm mới chưa từng có trong hệ**, và trục mới đó
**0 site nào đang canh** ⇒ rủi ro drift cao hơn.

## B. Enum persist + migration

### B.1 — CÓ persist, dạng `int` thuần, **4 cột** trên 4 bảng

| Nơi khai | file:line | Cột DB |
|---|---|---|
| `Contract.Type` | `Domain/Contracts/Contract.cs:11` | `Contracts.Type` |
| `WorkflowDefinition.ContractType` | `Domain/Contracts/WorkflowDefinition.cs:17` | `WorkflowDefinitions.ContractType` (V1) |
| `WorkflowTypeAssignment.ContractType` | `Domain/Contracts/WorkflowTypeAssignment.cs:11` | `WorkflowTypeAssignments.ContractType` (V1 override) |
| `ContractTemplate.ContractType` **nullable** | `Domain/Forms/ContractTemplate.cs:11` | `ContractTemplates.ContractType` (null = phụ lục) |

Cấu hình EF: `Configurations/ContractConfiguration.cs:15` `b.Property(x => x.Type).HasConversion<int>();`
(+ `WorkflowDefinitionConfiguration.cs`, `WorkflowTypeAssignmentConfiguration.cs`, `ContractTemplateConfiguration.cs` — 2 hit mỗi file).
Migration gốc: `20260421051248_AddContractsWorkflow.cs:33` → `Type = table.Column<int>(type: "int", nullable: false)`.
Snapshot: `ApplicationDbContextModelSnapshot.cs:882-883` `b.Property<int>("Type").HasColumnType("int")`.

### B.2 — 🟢 TRẢ LỜI: thêm giá-trị enum mới **KHÔNG cần migration**, giá-trị cũ `=1` **KHÔNG bị đụng**

Bằng chứng đo:
```
rg -c "HasCheckConstraint" src/Backend --glob '!**/Migrations/**'   => 0-hit (exit=1)
   CONTROL DƯƠNG cùng cây: rg -c "HasMaxLength" …/Configurations   => 54 file  ⇒ thước SỐNG, 0-hit là vắng thật

sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "
  SELECT c.name,t.name FROM sys.columns c JOIN sys.types t … WHERE object_id=OBJECT_ID('Contracts') AND c.name='Type'"
   => Type | int | is_nullable=0
  "SELECT COUNT(*) FROM sys.check_constraints WHERE parent_object_id=OBJECT_ID('Contracts')"  => 0
  "SELECT Type, COUNT(*) FROM Contracts GROUP BY Type"  => 1:2 · 2:1 · 3:1 · 4:1 · 5:1 · 6:1 · 7:1  (9 HĐ Dev)
```
⇒ Cột là `int` trần, **0 CHECK constraint**, EF `HasConversion<int>` chỉ đổi CLR↔int lúc đọc/ghi.
Thêm `= 8`/`= 9` vào enum C# ⇒ **snapshot EF KHÔNG đổi** ⇒ `dotnet ef migrations add` sinh migration **RỖNG**
(đúng khuôn Mig 62 `SupplierType +CaHai=6` append-only đã làm 1 lần rồi).
2 HĐ đang có `Type=1` giữ nguyên nghĩa "Thầu phụ (cũ)" — **không cần backfill** trừ khi owner muốn ép
9 HĐ cũ phải chọn 1 trong 2 biến-thể mới (đó là quyết định NGHIỆP VỤ, không phải ràng buộc kỹ thuật).

🔴 **Migration CHỈ phát sinh nếu** chọn A1 + đẻ bảng chi tiết riêng cho 2 loại mới (A.1-bis) — khi đó
+2 `CreateTable` ⇒ Mig 74, bảng 97→99. Nếu 2 loại mới **dùng lại `ThauPhuDetails`** ⇒ **0 migration**.

## C. 7 quy trình HD-* + guard

(đang đo)

## D. Rủi ro / điều CHƯA đo được (khai thật)

(đang đo)
