# spec — HĐ Thầu phụ tách 2 + form document-view + pre-fill 3 tầng (YC-032)

> Nguồn: `/fable-clone invest` 5 lane `investigator-codebase` (`wf_5f43c10f-506`) + lead refute/synthesize @P3 + 3 phép đo bù của lead.
> Ngày 2026-08-13 · phiên S194 · run `2026-08-13-S194-yc032-hd-thauphu-2form`.
> 🔴 File này do **LEAD ghi sau khi verify**, engine KHÔNG tự ghi.

---

## ① Tính-năng / đề-bài

Nguyên văn owner → `docs/governance/so-yeu-cau-chu-du-an.md` mã **`YC-032`**. Bốn vế:

1. **Tách** `01. HĐ Thầu phụ` thành 2 menu ruột riêng — *Thầu phụ (Nhân công & Vật tư)* ↔ `SOL-CCM-FO-002.02.v01` · *Thầu phụ (Nhân công & Thiết bị)* ↔ `SOL-CCM-FO-002.03.v01`.
2. **Đổi khuôn nhập** sang **document-view cuộn từ trên xuống** đúng thân hợp đồng — *"người nhập giống như đang view từ trên xuống toàn bộ cái hợp đồng"*.
3. **Pre-fill 3 tầng**: Duyệt NCC (đầy) → KHKK (đầy) → người nhập điền **nốt phần thiếu** ⇒ hợp đồng trọn, **chỉ để in trình ký**.
4. **Chốt bằng bản cứng**: upload bản đã ký → **KHÓA**.

**Phạm vi lượt này:** chỉ 2 HĐ đó. KHÔNG đụng 5 loại còn lại.

---

## ② Cách implement

### ②.0 — Nền đã có sẵn (đo được, đừng dựng lại)

| Thứ | Trạng thái | Bằng chứng |
|---|---|---|
| Chỗ chứa "văn bản trọn" | ✅ `Contract.DraftData nvarchar(max)` từ **Mig 5**, BE round-trip đủ | `Contract.cs:25` · `ContractFeatures.cs:105,146,207,851` |
| Bản cứng | ✅ `AttachmentPurpose.SealedCopy=3` + `HasSealedCopy` derived + endpoint + trang FE + **3 test** | `ContractAttachment.cs:9` · `ContractFeatures.cs:384-391` · `ContractsController.cs:105` · `HardCopyTests` |
| Khoá khi rời Nháp | ✅ sửa HĐ chỉ mở ở `DangSoanThao` | `ContractFeatures.cs:167-168` |
| Enum persist | ✅ `int` trần, **0 CHECK constraint** (control-dương `HasMaxLength`=54 file) | `ContractConfiguration.cs:15` · sqlcmd Dev |
| Guard họ workflow | ✅ **fail-closed**: loại ngoài họ ⇒ **409 có câu tiếng Việt** | `ContractWorkflowGuards.cs:147-152` |
| Tiền lệ số-hoá form giấy | ✅ 2 lần: `PeDetailTabs.tsx` 3.613 dòng · `KhkkDetailContent.tsx` 1.140 dòng | đếm thật |
| Render docx + PDF | ✅ `DocxRenderer` 81 dòng + LibreOffice converter + endpoint | `DocxRenderer.cs` · `LibreOfficePdfConverter.cs` |

### ②.1 — 🔴 Bốn cái BẪY phải né (mỗi cái đã cháy thật ở dự án này)

1. **`WorkflowPolicy.cs:155` hỏng CÂM.** `switch` liệt 3 loại rồi `_ => "SkipCcm"`. Thêm enum mà quên thêm vào vế `or` ⇒ **rơi im lặng, mất phase CCM, không nổ**. *(Đối chiếu: `ContractWorkflowGuards.cs:149` cùng tình huống lại **409 to tiếng** — hai switch cạnh nhau, hai kiểu hỏng ngược nhau. Chỉ cái CÂM mới nguy.)*
2. **`TYPE_CODE_TO_INT` có 4 bản chép tay trong FE.** `Layout.tsx:165` `if (!typeInt) return null` ⇒ thiếu 1 entry = **leaf biến mất khỏi sidebar, im lặng** (gotcha #50, đã cháy @S190).
3. **`draftData: null` hardcode 4/4 site FE** (`ContractCreatePage.tsx:366,:582` ×2 app) ⇒ **mỗi lần bấm Lưu là xoá trắng thân HĐ**. Phải gỡ TRƯỚC khi dựng document-view, nếu không mọi ô điền đều bay.
4. **Menu-key vào `All` thì BẮT BUỘC có menu-row** — `MenuKeys.cs:147-148`: thiếu row ⇒ **Permission FK nổ ngay lần boot đầu**. Thêm 2 member họ = +2 key vào `All` = +8 policy ⇒ phải seed row **cùng lượt**.

### ②.2 — Ba quyết định kiến trúc (lead chốt, có số đỡ lưng)

**(a) Tách bằng A1 — thêm 2 `ContractType` (=8, =9), KHÔNG dùng cờ biến-thể.**
A1 ≈ 13 site mã sống, **~6/13 by-construction** (sửa 1 mảng nguồn là nở hết: `ContractTypeCodes` → menu/seed-loop; `Family.Members` → menu-key/policy/guard/message). A2 ít sửa chỗ cũ hơn **nhưng đẻ một trục mới (`?variant=`) mà 0 site nào đang canh** ⇒ drift cao, và menu/route/codegen/template đều đang khoá bằng `ContractType` nên A2 phải nới 8-10 chỗ để dựng trục song song. **0 migration** nếu 2 loại mới dùng chung `ThauPhuDetails` (enum int trần + 0 CHECK ⇒ `ef migrations add` sinh migration RỖNG, đúng tiền lệ Mig 62 `SupplierType +CaHai=6`).

**(b) Document-view = HTML dựng tay làm TRỤC; docx-render chỉ làm đường XUẤT BẢN IN.**
Thứ nghe như "tái dùng" — render docx rồi overlay ô nhập — thực tế **thiếu 3 mảnh**: `DocxRenderer.cs:10` tự khai **không support vòng lặp bảng** (mà thân HĐ có bảng DANH MỤC N dòng) · 2 file gốc **chưa placeholder-hoá** (dạng `"Số: ………"`) · repo **0 viewer docx trong trình duyệt** (`contenteditable|tiptap|slate|quill` = 0 file, control-dương `useMemo` sống). ⇒ đi đường đó vẫn phải dựng UI nhập riêng = HTML **cộng thêm** một lớp.

**(c) Thân HĐ = component dùng chung + 2 bản LẮP RÁP — không phải 1 cờ, cũng không phải 2 bản chép tay.**
Đo của lead (so **tập dòng**, không so `diff`): `f02` 200 dòng không-rỗng → **133 dòng (66%) nằm nguyên trong `f03`**; `f03` 378 dòng → **245 dòng riêng**. ⇒ xương chung có thật và lớn, nhưng `f03` gấp ~1,9× nên một `bool` không chứa nổi.
🔸 *Đây là chỗ 2 lane mâu thuẫn: lane 5 "1 xương chung + delta" ⟂ lane 2 "KHÔNG phải sinh đôi, 1-thân-1-cờ là sai từ giả thiết" (căn cứ `diff` = 400 dòng). Cả hai đúng một nửa — `diff` không phân biệt "khác cấu trúc" với "cùng cấu trúc, thêm phần".*

**(d) Làm 1 app — `fe-user`.** 2 file `ContractCreatePage.tsx` hiện **giống nhau từng byte**, nhưng `fe-admin/Layout.tsx:234-249` đã ẩn trọn nhánh `Contracts` **có chủ đích** ⇒ mirror sang admin là công vô ích (màn sống-không-menu).

### ②.3 — 🔴 CHẶN: "toàn bộ cái hợp đồng" hiện KHÔNG tồn tại đủ

Mục lục **cả 2 file** khai `PHẦN 2 — ĐIỀU KIỆN CHUNG` gồm **ĐIỀU 1→17**; thân 2 file **không chứa**. Ứng viên duy nhất là `SOL-CCM-FO-002.04.v01 Dieu kien chung hop dong tron goi.docx`.
Lead đo (gộp `run` như `DocxRenderer` làm — tách run ra chỉ thấy 1 hit, đúng bẫy repo tự khai): bản repo ≡ bản gốc FORM **byte-identical 192.608 B** · `62 w:p · 0 bảng · 40 đoạn` · đúng **5 ĐIỀU**: 1 Am hiểu · 2 Nội dung công việc · 3 Tiến độ-chất lượng · **4 = "Xử lý tranh chấp" (mục lục gọi ĐIỀU 16)** · **5 = "Sửa đổi bổ sung" (mục lục gọi ĐIỀU 17)**.

⇒ **THIẾU 12 điều:** 4 Đơn giá · 5 Tiện ích công trường · 6 Phát sinh · 7 Nghiệm thu · **8 Tạm ứng & thanh toán** · 9 Quyền-nghĩa vụ · 10 Phạt · 11 Chấm dứt · **12 Bảo hành** · 13 Bất khả kháng · 14 Thông tin-thông báo · 15 Quy tắc ứng xử.
Nặng thêm: **bảng tóm tắt PHẦN 1 trỏ sang ĐIỀU 2/3/8/10/12 của PHẦN 2** ⇒ trỏ vào chỗ trống.

### ②.4 — Bản đồ pre-fill (nền của vế 3)

**24 ô đo được ở `002.02`: 17 có nguồn / 5 gõ tay / 2 chưa xác định — nhưng CHỈ 3 ô đã thực sự chảy tới hàng `Contracts` hôm nay** (`TenHopDong` · `GiaTri` · `MaHopDong`). 14 ô còn lại là **nguồn nằm sẵn trong DB, đường chưa nối** — đúng chỗ owner muốn "điền nốt".

| Cụm | Ô | Nguồn có sẵn | Trạng thái |
|---|---|---|---|
| Bên B | tên · địa chỉ · ĐT/Fax · số TK · MST · người đại diện+chức vụ | `Supplier.*` (6 cột) | ◻ chưa nối |
| Dự án | Chủ đầu tư · Công trình–tại | `Project.Investor` (Mig 48) · `Project.Name`+`Location` | ◻ chưa nối |
| Bảng giá | 7 cột `STT·Nội dung·ĐVT·Khối lượng·Đơn giá·Thành tiền·Ghi chú` | PE `Detail.*` + `Quote.ChuaVat/ThanhTien` → khớp `ThauPhuDetail` **6/7 cột 1-1** | ◻ nguồn có, **đứt 2 nhịp** |
| Thanh toán / bảo hành | tạm ứng · thanh toán · quyết toán · bảo hành | `pe.PaymentTerms` (JSON) | ◻ **regression**, xem dưới |
| Bên A | số TK · người đại diện | **0 bảng `CompanyProfile`** | ✍ gõ tay |
| Khác | ngày ký · tỷ giá · ngày hoàn thành tổng | 0 cột | ✍ gõ tay |
| Bằng chữ | "…… đồng" | **0 helper** (`numberToWords` 0-hit, control-dương `giaTri` 10 hit) | ◻ viết mới |

🔴 **Hai lỗ nghiêm trọng phát hiện kèm:**
- **REGRESSION cầu KHKK:** cầu PE→HĐ **cũ** có `DraftData = pe.PaymentTerms` (`CreateContractFromEvaluationFeatures.cs:104`) và `BypassProcurementAndCCM` (`:103`); cầu **KHKK→HĐ mới đánh rơi cả hai** ⇒ HĐ đi đường KHKK **mất sạch điều khoản thanh toán** mà HĐ đi đường PE vẫn có.
- **Sai tiền tiềm tàng:** `.doc` ghi giá trị **"bao gồm thuế VAT"**, còn `Contract.GiaTri` = Σ`ApprovedAmount` ← `Quote.ThanhTien` = KL × **`ChuaVat`** ⇒ **số hiện tại CHƯA VAT**. Bảng `.doc` lại có riêng dòng `VAT 10%` + `TỔNG SAU THUẾ`. Đổ thẳng vào ô "bao gồm VAT" là **sai tiền** — cần owner phán.
- **Quyết định cũ bị đảo:** `CreateContractFromEvaluationFeatures.cs:18-20` khai chủ đích *"KHÔNG copy Details per-type automatically — tránh mapping sai"*. YC-032 chính là lệnh **đảo** nó (ít nhất cho 2 loại TP) — phải khai rõ khi thi công.

### ②.5 — Bản cứng + khoá + in (vế 4)

- **Lưu bản cứng: 0 migration** — máy đã đủ.
- 🔴 **Nhưng KHÔNG khoá gì cả**: upload không đổi phase, không set cờ; xoá đính kèm là **HARD delete** ⇒ `hasSealedCopy` là **cờ 2 chiều**, xoá file là "mở khoá" lại. Owner nói *"upload rồi khóa"* = mốc **1 chiều** ⇒ lệch.
- 🔴 **Cửa GĐ4 hẹp hơn tưởng**: trang bản cứng chỉ nạp HĐ **phase 9 `DaPhatHanh`** và chỉ 4 role (`Director·AuthorizedSigner·HrAdmin·CostControl`) — **người soạn HĐ không có trong đó**.
- **Đường IN: 0 print CSS** cả 2 app (`window.print|@media print` = 0 hit, control-dương 56 file có `useState`). Render docx thì **đứt 3 chỗ**: 0 map `Contract → Dictionary` · 2 file `.docx` chưa có trên đĩa (seed `DbInitializer.cs:3833-3838` **đã khai** 002.02/002.03 nhưng `IsActive = fileExists` ⇒ **row tồn tại, cờ chết**) · `DocxRenderer` 0 vòng lặp bảng.
- ⚠️ Thả file vào sau **không tự bật cờ**: `:3861 if (existingCodes.Contains(formCode)) continue` ⇒ wave nào thả file phải kèm **UPDATE `IsActive=1`**.

---

## ③ Checklist theo WAVE

> Acceptance mỗi wave phải **ĐO ĐƯỢC**. Vai gợi ý; lead điều phối.

### W0 — CHỜ OWNER (chặn một phần, không chặn hết)
- [ ] 5 câu ở §"Chờ anh" của báo cáo. **W1/W2 chạy song song được** với W0 (không phụ thuộc đáp án); **W3/W4 phụ thuộc**.

### W1 — Tách 2 loại (BE + menu + FE dict) · `implementer-backend` + `implementer-frontend`
- [ ] +2 member `ContractType` (=8, =9) + **+2 vào vế `or` của `WorkflowPolicy.cs:155`** ← bẫy CÂM
- [ ] +2 tuple `ContractApprovalWorkflowFamily.Members` ⇒ họ 7→9 (menu-key + policy tự nở by-construction)
- [ ] +2 `MenuKeys.ContractTypeCodes` · +2 nhánh `ContractCodeGenerator.cs:27` · +2 nhãn `ContractExcelExporter.cs:36` + `WorkflowAdminFeatures.cs:72`
- [ ] Seed: menu-row `Ct_*` ×2 + `Hdc_*` ×2 + nhãn tiếng Việt 2 chỗ (`DbInitializer.cs:2054` + `:2375`) + **seed 2 quy trình `HD-*` mới**
- [ ] FE: **cả 4 bản** `TYPE_CODE_TO_INT` (2 `Layout.tsx` + `ApprovalWorkflowsV2Page.tsx:172` + `WorkflowsPage.tsx:77`)
- [ ] **Acceptance:** `dotnet ef migrations add` sinh migration **RỖNG** (chứng enum không đụng schema) · boot Dev **không nổ Permission FK** · 2 leaf mới **hiện trên sidebar** (chống gotcha #50) · pin chéo quy trình ⇒ **409** · test hiện có vẫn **697 PASS**

### W2 — Khung document-view (2 thân) · `frontend-designer` + `implementer-frontend`
- [ ] **Gỡ `draftData: null` 4/4 site TRƯỚC** ← nếu không mọi ô đều bay khi Lưu
- [ ] Component thân dùng chung (bìa · mục lục · căn cứ · Bên A/B · ĐIỀU 2 định-nghĩa · ĐIỀU 4 · điều khoản chung · bảng tóm tắt 8 dòng đầu)
- [ ] 2 bản lắp ráp: `002.02` (5 điều, bảng giá nhúng ĐIỀU 3, 14 dòng tóm tắt) · `002.03` (6 điều +Thuế, bảng ĐIỀU 3 rỗng→PL02, 16 dòng tóm tắt, **4 phụ lục**)
- [ ] Ô lặp **bind 1 nguồn**: Số ×2 · Hạng mục ×2 · Địa điểm ×2 · **tên Bên B ×3** · Dự án ×2 (f03)
- [ ] Lưu vào `Contract.DraftData` (JSON) — **0 migration**
- [ ] **Acceptance:** nhập → Lưu → F5 → **đủ ô còn nguyên** (chống bẫy #3) · ô máy-điền ⟂ ô phải-gõ **phân biệt được bằng mắt** · `npm run build` ×1 app sạch

### W3 — Pre-fill nối 14 ô · `implementer-backend` (phụ thuộc W0)
- [ ] Nối `Supplier.*` 6 ô + `Project.Investor/Name/Location` 2 ô
- [ ] Bảng giá 7 cột: PE `Detail`+`Quote` → `ThauPhuDetail` (đọc **vượt tầng** về PE qua `plan.PurchaseEvaluationId` — đường **đã mở sẵn** ở `CreateContractFromSigningPlanFeatures.cs:199-200`)
- [ ] **Vá regression**: cầu KHKK gán lại `DraftData = pe.PaymentTerms` + `BypassProcurementAndCCM`
- [ ] Helper `numberToWords` (tiền → chữ)
- [ ] **Acceptance:** tạo HĐ từ KHKK thật trên Dev → đếm ô tự điền **≥14** · so từng ô với nguồn **byte-exact** · **ĐO DỮ LIỆU TRƯỚC**: `COUNT(PE có PaymentTerms != null)` — cơ-chế đúng mà dữ-liệu rỗng đã lặp nhiều lần ở dự án này

### W4 — Đường IN · `implementer-frontend` (phụ thuộc W0 câu 2)
- [ ] Print CSS cho document-view (`@media print`: ẩn chrome, ngắt trang theo ĐIỀU, giữ bảng không vỡ)
- [ ] **Acceptance:** Ctrl+P ra PDF **khớp bố cục bản Word** — đối chiếu bằng mắt 2 trang đầu + trang bảng giá

### W5 — Khoá bản cứng · `implementer-backend` + `database-agent` (phụ thuộc W0 câu 3)
- [ ] Theo đáp án owner: **(0-mig)** guard mềm dựa `HasSealedCopy` · **(1-mig)** `Contracts.SealedAt` + `SealedByUserId` = mốc 1 chiều, audit được
- [ ] Nới cửa GĐ4 nếu owner muốn người soạn thấy (nay 4 role đóng + phase 9)
- [ ] Mirror endpoint `view` inline từ KHKK (`ContractSigningPlansController.cs:254`) — HĐ hiện thiếu
- [ ] **Acceptance:** upload bản cứng → sửa HĐ ⇒ **409** · xoá đính kèm ⇒ **không mở khoá lại** · test-before cho cả 2 ca

### W6 — Verify + deploy · `reviewer` + `cicd-monitor`
- [ ] `reviewer` adversarial toàn diff · `front-end-reviewer-style` + `back-end-reviewer-style`
- [ ] **Acceptance:** test ≥697 PASS · cicd 6/6 · bundle hash đổi ×2 app · **runtime-verify console 0 lỗi** (bài gotcha #92: mọi phép đo chỉ đo FILE thì 2 lần đều lọt)

---

## ④ Điều CHƯA đo được (khai thật, đừng đọc thành đã phủ)

1. **Mục C+D của lane-1** (`sub-investigator-codebase-1.md`) còn `(đang đo)` — cháy trần lượt. Lead **đã đo bù mục C** (guard fail-closed) nhưng **mục D "rủi ro" thì chưa ai làm**.
2. **Prod có HĐ Thầu phụ sống không** — chưa đo lượt này. Memory S160 ghi "prod Contracts = 0 active" nhưng đã **cũ**; phải đo lại nếu dùng để quyết migration/backfill.
3. **Chất lượng convert `.doc → .docx`** của LibreOffice với 2 file 516KB/591KB (bảng phức tạp, header 3 tầng merge) — **chưa chạy thử**.
4. **`soffice` trên prod**: lane-2 báo `Test-Path` = True; **máy dev không có** ở đường mặc định ⇒ nhánh IN server-side sẽ chết câm ở local. `Pdf:SofficePath` **0-hit trong mọi `*.json`** = ghost-wire.
5. **`antiword` cắt UTF-8 ở biên wrap dòng** (f02 3 dòng, f03 5 dòng; `U+FFFD` = 0 nên **trông như sạch**, chỉ decode mới lộ) ⇒ 🔴 **CẤM chép thẳng text antiword vào code**; câu chữ chuẩn phải lấy qua Word/LibreOffice.
6. **Số ô THẬT** có thể > 24 — bảng §②.4 là **cận dưới**, chưa gộp ô nằm trong 61/113 dòng bảng.

---

## ⑤ VÁ SAU REVIEW — `/fable-clone review` 4 lane, **40 finding (19 nặng)**

> Ghi APPEND, **KHÔNG sửa đè** §①-④ ở trên: giữ bản gốc làm vết để đối chiếu spec-trước ⟂ spec-sau.
> Nguồn: `sub-reviewer-1..4.md`. 🔴 **CẤM giao thợ bản §①-④ chưa đọc mục này.**

### ⑤.a — 4 chỗ spec NÓI SAI (lead tự đo lại, xác nhận)

| # | Spec §①-④ nói | Sự thật đo được | Neo |
|---|---|---|---|
| S-1 | *"người soạn HĐ không nằm trong 4 role thấy trang bản cứng"* | **SAI** — `DrafterUserId == userId` là **vế OR ĐẦU TIÊN**; 4 role là vế **NỚI THÊM**. ⇒ W5 "nới cửa GĐ4" là việc trên tiền đề sai | `ContractFeatures.cs:347` |
| S-2 | *"upload bản cứng xong vẫn sửa được HĐ"* | **SAI** — bản cứng chỉ tồn tại ở phase 9, mà sửa HĐ **đã khoá từ khi rời `DangSoanThao`** ⇒ acceptance W5 "upload → sửa ⇒ 409" **PASS sẵn, 0-bit** | `ContractFeatures.cs:167-168` |
| S-3 | *"W1 và W2 chạy song song được"* | **SAI** — acceptance W2 không có đường chạy khi chưa có 2 loại (W1); cộng vế tranh `DraftData` ⇒ **không cặp wave nào song song thật** | R4-F06 · R2-F4 |
| S-4 | *"`GiaTri` ← `ThanhTien` = KL × `ChuaVat` ⇒ chưa VAT"* | **SAI** — `ChuaVat` là **cột chết** (UI gửi 0; **12/34 dòng Dev = 0**); `ThanhTien` là số **gõ tay, không khai VAT** ⇒ hỏi owner theo khuôn cũ = rủi ro **cộng VAT 2 lần** | `PeDetailTabs.tsx:2959-2962` |

🔸 S-1/S-2 cùng một gốc: lane invest đọc **trang FE** (`HardCopiesPage` lọc phase 9 + 4 role) rồi suy ngược ra luật BE, mà **BE rộng hơn FE**. Bài: *đọc bộ lọc của một màn hình không bằng đọc câu `Where` của truy vấn.*

### ⑤.b — 5 lỗ CHẶN mới (không lỗ nào có wave)

| # | Lỗ | Neo |
|---|---|---|
| B-1 🔴 | **W2 ⟂ W3 tranh CÙNG cột `DraftData`** — W2 ghi thân HĐ, W3 ghi `pe.PaymentTerms` ⇒ parse nhầm + đè chết. Phải chốt **bao-ngoài + `schemaVersion` TRƯỚC W2** (Dev đang 0 row = thời điểm rẻ nhất) | R2-F4 · `PurchaseEvaluation.cs:29` |
| B-2 🔴 | **HĐ tự mâu thuẫn về tiền**: bảng giá Σ`PeReferenceAmount` ⟂ ô Giá trị Σ`ApprovedAmount`; Dev **2/2 lệch 100tr & 50tr** | R3-F7 · `…FromSigningPlanFeatures.cs:202,223,226-229` |
| B-3 🔴 | **5/8 ô master IN TRẮNG**: TK 83% · người đại diện 83% · chủ đầu tư 93% · địa điểm 93% · MST 25% rỗng. Đổi C4 sang `pe.DiaDiem` (**0% rỗng**) | R3-F5b |
| B-4 🔴 | **Upload/xoá đính kèm: 0 policy · 0 phase-guard · 0 ownership** (chỉ `[Authorize]` trần ở class) ⇒ phương án khoá-mềm của W5 để **bất kỳ ai khoá/mở khoá HĐ người khác** | R4-F02 · `ContractsController.cs:105,:130` |
| B-5 🔴 | **Template rỗng sau khi tách loại**: 2 row seed cứng `ContractType.HopDongThauPhu`; FE+BE lọc template theo type ⇒ tách xong **dropdown Mẫu RỖNG**. Seeder skip-theo-FormCode **không tự sửa** | R2-F7 · `DbInitializer.cs:3833,3861` · `FormFeatures.cs:33` |

### ⑤.c — 3 việc BẮT BUỘC mà 0 wave nào nhận

1. **Đưa 2 file mẫu vào hệ**: bản trên đĩa là **`.doc`**, seed khai **`.docx`**, `wwwroot/templates` **không có** ⇒ **convert + chép + `UPDATE IsActive=1`** = 3 việc (R4-F04).
2. **Số hoá 12 điều thiếu** của PHẦN 2 — không wave nào làm, cũng **không có nhánh dự phòng** nếu owner không cấp file (R4-F05).
3. **Sản xuất bản chữ CHUẨN** — §④.5 cấm dùng text `antiword` (cắt UTF-8 ở biên wrap), nhưng **không wave nào sinh ra bản chữ sạch** để dựng TSX (R4-F05).

### ⑤.d — Acceptance phải viết lại (2/5 của W1 hỏng, W2 hụt 6 lớp ca, W5 0-bit)

- **W1-A4 PASS VACUOUS**: cùng một hàm ném 409 cho **cả** ca-pin-chéo **lẫn** ca-quên-khai ⇒ xanh ở 2 thế giới. Phải có **ca DƯƠNG** (pin đúng mã loại mới ⇒ 200) + phân biệt **đúng CÂU lỗi**, không chỉ đúng mã HTTP.
- **W1-A5 SAI BY-CONSTRUCTION**: `AwV2DesignerMenuKeyTests` neo cứng `HaveCount(7)`/`HaveCount(17)` ⇒ **không tồn tại đường "thêm 2 loại mà 697 giữ nguyên"**. Viết *"697+k, khai k, cho phép sửa ĐÚNG method nào, cấm xoá/skip"*.
- **W2** hụt 6 lớp ca (ô lặp · bảng N dòng · số/tiền dẫn xuất · ngày 3 mảnh · **xoá về rỗng** · bằng chữ) + **thiếu ca ÂM** (sửa ô header cũ ⇒ `DraftData` KHÔNG được về null). Bỏ tiêu chí "phân biệt bằng mắt" (không đo được).
- **W5-A1 0-bit** (S-2 ở trên) — thay bằng ca thật: **xoá file bản cứng ⇒ KHÔNG mở khoá lại**.
- **W3** thay ngưỡng "≥14 ô" (đặt trên tập chỉ có **13** phần tử) bằng **bảng điểm danh 13 dòng** + cấm bảng-giá-rỗng qua bài.

### ⑤.e — 3 nhánh CÂM nữa (ngoài `WorkflowPolicy.cs:155` đã biết)

Lý do sót: spec chỉ grep `case ContractType` ⇒ **bỏ trọn switch-EXPRESSION (`=>`)**. C# có 2 cú pháp switch; grep 1 là đo nửa.
- 🔴 `ContractCodeGenerator.cs:28` `_ => "HĐ"` ⇒ **mã HĐ sai chuẩn RG-001, KHÔNG hồi tố được** (`ContractCodeSequences` đã tăng).
- `ContractDetailsFeatures.cs:41-113` switch **không default** ⇒ bundle RỖNG trả **HTTP 200** (câm).
- `ContractDetailsTab.tsx:583-597` switch không default ⇒ **payload rụng sạch field**.

### ⑤.f — Đồ thị phụ thuộc ĐÚNG (thay câu "W1 ∥ W2" đã bị bác)

```
B-1 chốt lược đồ DraftData  ─┐
                             ├─→ W2 (khung) ──┐
W1 (tách 2 loại) ────────────┘                ├─→ W4 (in) ─→ W6
                             └─→ W3 (pre-fill)┘
⑤.c-1 (đưa mẫu vào hệ) ──→ B-5 (vá template type) ──→ W2
⑤.c-2/3 (12 điều + bản chữ chuẩn) ──→ W2   [CHẶN: chờ owner câu (97)]
B-4 (authz đính kèm) ──→ W5
```
⇒ **W1 là wave DUY NHẤT khởi động được ngay**; mọi wave khác có tiền đề.

### ⑤.g — Bộ câu hỏi owner ĐÃ SỬA

| Cũ | Phán của review | Mới |
|---|---|---|
| (97) 12 điều thiếu | ✅ giữ — R4 đo lại độc lập, **xác nhận** | giữ nguyên |
| (98) in bằng gì | ❌ **THỪA** — W4 đã chốt Print CSS **trước khi** hỏi | **RÚT** |
| (99) khoá 0-mig/1-mig | ❌ **SAI KHUÔN** — hỏi owner chọn cách cài đặt | → *"bản cứng đã upload có được **xoá/thay** không, và **ai** được làm?"* |
| (100) VAT | ❌ **SAI KHUÔN** (S-4) | → *"giá nhập ở Duyệt NCC là **đã gồm** hay **chưa gồm** VAT?"* |
| (101) đánh số menu | ✅ giữ | giữ nguyên |
| — | 🔴 **QUÊN HỎI** | **(102)** *"**ai** được bấm upload bản cứng?"* — quyết cả authz (B-4) lẫn cửa GĐ4 |

---

## ⑥ BẢN THI CÔNG — sau khi owner phán 5/5 câu (`YC-033` + `YC-034`, 2026-08-14)

### ⑥.a — 5 phán quyết của owner

| Câu | Owner chốt | Hệ quả thi công |
|---|---|---|
| **(97)** PHẦN 2 | *"à có nhé, file này"* → `002.04`; *"cho nó phần cuối là đc"* | document-view = **PHẦN 1 riêng** (`002.02`/`002.03`) **+ PHẦN 2 gắn CUỐI** (`002.04`, **dùng chung 2 loại**). 🔴 Đo lại bằng thước đã vá: `002.04` = **5 điều**, KHÔNG phải 17 ⇒ chặn gỡ bằng **thu hẹp phạm vi**, không phải bằng tìm ra 12 điều |
| **(99)** khoá | *"upload có quy trình duyệt riêng → duyệt nhanh, nhưng đã chốt rồi ko sửa đc"* | Khoá = **BẤT-KHẢ-ĐẢO**, không phải cờ 2 chiều. Quy trình duyệt bản cứng = **để sau** (kèm `(102)`) ⇒ lượt này chỉ dựng **chốt-cứng** |
| **(100)** VAT | *"hiện là chưa VAT nhưng chỗ này để sau"* | KHÔNG dựng xử-lý VAT. Ghi **nợ đã-biết**: thân HĐ ghi *"bao gồm VAT"* mà số đang chưa VAT |
| **(101)** đánh số menu | *"Tùy mày"* | Lead chọn **`01A.` / `01B.`**, giữ nguyên `02.`→`07.` (đánh lại 01→09 sẽ đổi số 6 menu người dùng đang quen; bộ số này đã sửa theo UAT anh Kiệt @S159) |
| **(102)** ai upload | *"cho nhân viên → có quy trình riêng nhưng để sau"* | KHÔNG dựng phân quyền upload lượt này. Lỗ `B-4` (endpoint 0 policy/0 ownership) **giữ KHAI là nợ đang mở** |

### ⑥.b — Lead chốt lược đồ `DraftData` (gỡ B-1, tiền đề của W2+W3)

Dev đang **0 row** `DraftData` ⇒ thời điểm rẻ nhất để đặt bao ngoài. Chốt:

```jsonc
{ "schemaVersion": 1,
  "kind": "HD-TP-VT" | "HD-TP-TB",     // biết thân nào để render
  "body":  { "<fieldKey>": "<giá trị ô trong thân HĐ>" },   // W2 sở hữu
  "paymentTerms": { … }                 // W3 sở hữu — chỗ pe.PaymentTerms đổ về
}
```
⇒ W2 và W3 **hết tranh cột**: mỗi wave sở hữu một nhánh, đọc/ghi qua nhánh của mình. `schemaVersion` để đổi thân về sau không phải đoán.

### ⑥.c — Nợ đã-biết (owner đã thấy, CỐ Ý chưa làm — KHÔNG phải bỏ sót)

1. **VAT** — thân ghi "bao gồm VAT", số chưa VAT · owner: để sau.
2. **Con trỏ chết** — bảng tóm tắt PHẦN 1 trỏ *"xem ĐIỀU 8 / ĐIỀU 12"* của PHẦN 2, mà `002.04` **không có 2 điều đó** · owner chấp nhận đi tiếp.
3. **Authz đính kèm** — upload/xoá 0 policy · 0 ownership · owner: quy trình riêng để sau.
4. **12 điều** của PHẦN 2 theo mục lục · phạm vi thu hẹp còn 5 điều của `002.04`.

### ⑥.d — Thứ tự thi công CHỐT (thay đồ thị §⑤.f)

| Nhóm | Việc | Tiền đề |
|---|---|---|
| **G1** *(chạy ngay, song song thật)* | **W1** tách 2 loại (**17 site**, không phải 13) · **⑤.c-1** convert 2 `.doc`→`.docx` + chép `wwwroot/templates` + `UPDATE IsActive=1` + **B-5** vá `ContractTemplates.ContractType` sang 8/9 | 0 |
| **G2** | **W2** document-view (PHẦN 1 riêng ×2 + PHẦN 2 chung ở cuối) + gỡ `draftData:null` **4/4 site** + `MaximumLength` cho `DraftData` | G1 · ⑥.b |
| **G3** | **W3** pre-fill 13 ô + đổi C4 sang `pe.DiaDiem` + vá regression `PaymentTerms` + **B-2** chốt bảng giá đọc số nào để Σ khớp `GiaTri` | G2 |
| **G4** | **W4** print CSS · **W5** khoá bất-khả-đảo (`SealedAt` + cấm xoá/thay file sau chốt) | G2 · G3 |
| **G5** | **W6** reviewer + style ×2 + cicd + runtime-verify | tất cả |

🔴 **Acceptance đã viết lại** theo §⑤.d — đặc biệt: W1 **KHÔNG** dùng "697 PASS giữ nguyên" (sai by-construction) mà **"697+k, khai k, chỉ được sửa `AwV2DesignerMenuKeyTests` 3 method neo số, cấm xoá/skip"**; W5 **KHÔNG** dùng "upload → sửa ⇒ 409" (PASS sẵn) mà **"xoá file bản cứng ⇒ KHÔNG mở khoá lại"**.
