# SUB-INVEST-BE — Module "Duyệt Hợp đồng" (GĐ3) kế thừa từ KHKK — YC-023

> Run `2026-08-10-S187-duyet-hd-tu-khkk` · vai `investigator-codebase` · ENGINE `/fable-real` (H21 lệnh-A, single deep-pass, propose-only) · lăng kính **BE**.
> Đề bài: đề xuất CÁCH LÀM (không code) — cấu trúc BE tương tự KHKK + PE, quy trình duyệt **KHÁC NHAU theo LOẠI hợp đồng**, phiếu **kế thừa từ KHKK đã duyệt**.
> Mọi claim kèm file:line đọc THẬT trong phiên này (2026-08-10). Câu nghiệp-vụ = CHỜ-ANH, không đoán.
> File ghi TỪNG MỤC trong lúc điều tra (chống #53 — artifact đổ-ruột-liên-tục).

---

## §0 Phương pháp + phạm vi đọc

Đã đọc thật (không trích trí nhớ): `Contract.cs` · `ContractPhase.cs` · `ContractSigningPlan.cs` · `ContractSigningPlanLine.cs` · `ContractSigningPlanPhase.cs` · `ApprovalWorkflow.cs` (3 entity V2 + enum) · `CreateContractFromSigningPlanFeatures.cs` (bridge K7 trọn 251 dòng) · `ContractWorkflowService.cs` (:1-120 + grep V2) · `docs/STATUS.md` dòng CURRENT. Còn lại đọc ở các mục dưới (mỗi mục ghi ngay khi xong).

**Ground-truth mở màn (2 con số cần chỉnh so với brief):**
- Brief ghi "Mig 71" — đĩa có **Mig 72** `20260806070242_AddMaHangMucToContractSigningPlanLines` (S177, denorm `MaHangMuc` cặp với `TenHangMuc` — `ContractSigningPlanLine.cs:29-36`). Điều tra này lấy Mig 72 làm mốc.
- STATUS dòng sống S185: 668 test · Policies 452 · Menu keys 113 (`docs/STATUS.md:6`).

---

## §1 Q1 — Tái dùng được gì NGAY: Contract V2 wire tới đâu

### 1a. Contract V2 = ĐÃ WIRE ĐỦ VÒNG DUYỆT (approve-side), từ S29

| Mảnh | Bằng chứng | Trạng thái |
|---|---|---|
| Pin V2 trên phiếu | `Contract.cs:23` `ApprovalWorkflowId` (Mig 32) + `:46` `CurrentApprovalLevelOrder` — con-trỏ ĐÔI y PE | ✅ có |
| Service duyệt V2 | `ContractWorkflowService.cs:111-116` branch `ApprovalWorkflowId is Guid` → `ApproveV2Async` (`:230`, comment `:218` "Mirror PE :446-634") | ✅ có |
| Ý kiến cấp duyệt | `Contract.cs:56` `List<ContractLevelOpinion>` (Mig 33, UPSERT từ ApproveV2Async) | ✅ có |
| State machine 5 trạng thái | `ContractPhase.cs:14-28`: DangSoanThao=2 · ChoDuyet=10 · DaPhatHanh=9 (terminal + gen mã) · TraLai=98 · TuChoi=99; 7 giá trị LEGACY giữ cho data cũ (`:16-23`) | ✅ có |
| Trình/gửi lại | `ContractWorkflowService.cs:70-102` — 2 entry (Nháp ∨ TraLai) → ChoDuyet, reset con-trỏ, vai nới 4 vế Drafter∨DeptManager∨Procurement∨creator (`:84-88`, W6 S161) | ✅ có |
| Codegen RG-001 | gen tại bridge/tạo (`CreateContractFromSigningPlanFeatures.cs:212`) — HĐ từ cầu K7 có mã NGAY lúc tạo | ✅ có |
| Bridge từ GĐ2 | `CreateContractFromSigningPlanFeatures.cs` TOÀN BỘ (K7, commit `aaed6992`) — chi tiết §3 | ✅ có |

### 1b. `ApplicableType` KHÔNG phân theo ContractType — đây là CÁI THIẾU đúng tâm đề bài

- Enum `ApprovalWorkflowApplicableType` (`ApprovalWorkflow.cs:53-71`): `Contract = 3, // HĐ general (any ContractType)` (`:57`) — **MỘT giá trị cho cả 7 loại HĐ**. KHKK có slot riêng `ContractSigningPlan = 10` (`:70`).
- Chính bridge K7 tự khai gap này thành chữ: *"`ApprovalWorkflow.ApplicableType` = Contract(3) là 'HĐ general (any ContractType)' ⇒ **KHÔNG suy được từ ContractType, người bấm phải chọn trong dialog**"* (`CreateContractFromSigningPlanFeatures.cs:23-25`); command bắt buộc client truyền `ApprovalWorkflowId` (`:46-49`, `:54`).
- Guard pin ở bridge chỉ check 4 vế: tồn tại ∧ `ApplicableType==Contract` ∧ `IsActive` ∧ `IsUserSelectable` (`:134-148`) — **KHÔNG có vế nào so quy trình với `ContractType`** ⇒ hôm nay user có thể pin quy trình "dành cho thầu phụ" vào HĐ nguyên tắc mà máy không cản (vì máy không biết quy trình nào dành cho loại nào).
- Đối chiếu: máy per-type ĐANG TỒN TẠI ở **V1** — `WorkflowDefinition` có cột `ContractType` và bridge vẫn pin V1 active-per-type song song (`CreateContractFromSigningPlanFeatures.cs:163-166`). Tức là "quy trình theo loại HĐ" là khái niệm V1 đã có, V2 làm rơi mất khi gộp về `Contract=3`.

**Kết luận Q1:** máy duyệt V2 cho HĐ tái dùng NGAY 100% (service + con-trỏ + LevelOpinions + state machine + bridge); mảnh thiếu DUY NHẤT đúng đề bài = **quan hệ "quy trình ↔ loại HĐ" ở tầng V2** (chọn đúng quy trình theo loại + rào không cho pin chéo loại) — và tầng hiển thị cho approver (xem §1c bổ sung sau khi soi inbox).

### 1c. ĐÍNH CHÍNH điều tra cũ S156: tầng hiển thị approver V2 ĐÃ VÁ @W6 S161

- Kết luận S156 *"GetEligiblePhases + inbox 100% LEGACY ⇒ approver V2 mù HĐ ChoDuyet"* **HẾT HIỆU LỰC**: `GetMyInboxQuery` nay V2-aware — `ResolveV2InboxIdsAsync` (`ContractFeatures.cs:559-593`, in-memory join CÓ CHỦ ĐÍCH vì `CurrentWorkflowStepIndex` là INDEX 0-based ≠ `Order`, giữ OR-of-N `:588-589`), điều kiện thoát sớm xét CẢ 2 nguồn (`:536-538`). List + Detail có vế V2 (`:309-316` + `:623-628` membership-403); ListDeleted CỐ Ý KHÔNG ghép vế V2 (`:442-447` — chống rò HĐ xóa mềm).
- 3 cái CÒN thiếu thật sự (ngoài per-type §1b):
  1. **Pin V2 ở Create là OPTIONAL** (`ContractFeatures.cs:37` `Guid? ApprovalWorkflowId = null`) — HĐ tạo không pin rơi về V1/policy hardcode. Bridge K7 thì ÉP pin (`CreateContractFromSigningPlanFeatures.cs:63` NotEmpty) — HĐ sinh từ GĐ2 luôn V2. Nếu GĐ3 chỉ đi đường bridge thì gap này không cắn; đường `POST /api/contracts` tay vẫn hở.
  2. **Guard pin ở Create thiếu 2 vế `IsActive ∧ IsUserSelectable`** (`:79-87` chỉ check tồn-tại + type) — bridge K7 đã bịt cho đường nó (`:141-148`, vá-3), đường Create thường chưa.
  3. **KHÔNG có đường re-pin quy trình trên HĐ Nháp/Trả-lại**: `UpdateContractDraftCommand` (`ContractFeatures.cs:136-145`) không có field `ApprovalWorkflowId` — phiếu pin nhầm quy trình chỉ còn nước xóa-tạo-lại. Đối chiếu KHKK: có re-pin ở UpdateDraft + rào nhóm 3 cửa, và message rào chỉ dẫn đúng đường đó (`ContractSigningPlanWorkflowGuards.cs:76-77` *"vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình"*). GĐ3 per-type SẼ CẦN cửa này (pin sai loại → phải sửa được).

---

## §2 Q2 — Map "quy trình KHÁC NHAU per-LOẠI-HĐ" vào máy hiện có

### 2a. Bẫy chữ "LOẠI hợp đồng" — 2 nghĩa, phải chốt trục trước khi build

Spec-memory SOL-CCM-SP-002 (`.claude/auto-memory/project_sol_ccm_sp002_signing_matrix.md:41`) cảnh báo tường minh: *"Từ 'loại HĐ' trong lời anh mang 2 nghĩa"*:
- **Nghĩa A — 7 `ContractType` hệ thống** (Thầu phụ · Giao khoán · NCC · Dịch vụ · Mua bán · Nguyên tắc NCC · Nguyên tắc DV): khớp chữ YC-023 ("thầu phụ, nguyên tắc, …") + sidebar GĐ3 đã fan-out 7 mục (memory `:45`, anh xác nhận hình prod).
- **Nghĩa B — 8 nhóm duyệt N1-N8** nén từ 86 dòng danh mục SP-002 (chuỗi ký nháy + người ký chính đi theo trục NÀY, không theo 7 type): đã ăn vào GĐ2 (`ContractCatalogEntry.ApprovalGroup` 1..8 → `ContractSigningPlan.ApprovalGroup` → 8 workflow `KHKK-N{n}`).
⇒ **CHỜ-ANH #1**: GĐ3 phân quy trình theo trục nào — 7 ContractType (mặc định theo chữ YC-023) hay 8 nhóm SP-002 (như GĐ2), hay lai (số trạm theo nhóm, người ký chính theo ngưỡng UQ)? Chọn sai trục = dựng 7 workflow rồi phát hiện ma trận ký cần 8 nhóm. Điều tra dưới đây viết theo Nghĩa A, và cả 3 phương án đều đổi trục được vì discriminator là tham số.

### 2b. Khuôn per-group KHKK — cái ĐÃ CHẠY, mirror được gần nguyên khối

Cơ chế 4 mảnh (đều đọc thật):
1. **Discriminator trên phiếu**: `ContractSigningPlan.ApprovalGroup` int 1..8 (`ContractSigningPlan.cs:41`, Mig 71 backfill=1).
2. **Họ workflow cùng type khác Code**: 8 bản `KHKK-N{n}` CÙNG `ApplicableType=10`, seed idempotent **PER-CODE** (`DbInitializer.cs:540-542` — CỐ Ý né khuôn check-per-type vì *"check theo type sẽ chặn 7 nhóm còn lại"*; `:607-625`); resolve người duyệt theo **EMAIL cứng 11 người**, thiếu user → bỏ slot + warning, workflow 0-bước vẫn tạo (`:544-548`, `:587-600`).
3. **Rào khớp nhóm DÙNG CHUNG 3 cửa** (tạo / sửa-nháp re-pin / trình): `ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync` (`ContractSigningPlanWorkflowGuards.cs:65-78`), parse nhóm từ Code bằng regex 1-nhà-duy-nhất (`:44-45`), code NGOÀI họ cho qua CÓ CHỦ ĐÍCH (`:56-60`). Sinh ra @S182 W1b vì bản 1-cửa bị bypass 2-bước (`:9-12`).
4. **Vá admin-side S182 ĐÃ LAND**: normalize Code tại đường ghi (`ApprovalWorkflowV2AdminFeatures.cs:348` — chặn `khkk-n1` CI_AS-collision, `:335-347`); deactivate-khi-tạo-version chuyển **per-CODE riêng cho type-10** (`:356-376`).

### 2c. 🔴 2 quả bom per-type CÒN NGUYÊN cho type-3 nếu mirror họ-Code

- **Bom 1 — deactivate per-TYPE**: nhánh per-Code ở `ApprovalWorkflowV2AdminFeatures.cs:370-376` hard-wire `typeEnum == ContractSigningPlan`; **type-3 vẫn đi nhánh `:374-376` tắt IsActive per-TYPE**. Để nguyên mà seed 7 workflow `HD-*` cùng type-3 ⇒ admin tạo version mới cho MỘT loại HĐ là **tắt active CẢ 7 loại** (đúng bug S182 của KHKK tái sinh). Comment `:363` còn ghi *"các loại khác: per-TYPE như cũ — 1 active/type là bất biến của chúng"* — bất biến này PHẢI gỡ cho type-3 trước khi seed họ.
- **Bom 2 — overview Active-đơn**: `AwTypeSummaryDto` lấy `versions.FirstOrDefault(v => v.IsActive)` per type (`:205-215`) — 7 active cùng type-3 sẽ hiện 1 giấu 6 (đúng bệnh "giấu 7/8" S182). DTO/panel cần nhánh family cho type-3 như type-10 đã xử.

### 2d. 3 phương án + khuyến nghị xếp-bằng-chứng

| | PA-A: mirror KHKK họ-Code (0-mig) | PA-B: cột thật trên ApprovalWorkflows (1-mig) | PA-C: giữ chọn-tay (0 đổi máy) |
|---|---|---|---|
| Cách | Họ Code `HD-{...}` ×7 cùng type-3; discriminator phiếu = `Contract.Type` **CÓ SẴN** (KHKK phải đẻ cột Mig 71 — HĐ khỏi); guard mirror `EnsureWorkflowGroupMatchAsync` (parse type từ Code, áp 2 site pin hiện có: Create + bridge, + site re-pin mới §1c-3); seeder mirror `SeedKhkkGroupWorkflowsAsync` per-Code | Thêm cột nullable phân-nhánh-trong-type (generic, vd SubKey int?) trên `ApprovalWorkflows`; type-3 ghi ContractType; guard so CỘT; deactivate per (Type, SubKey); về sau migrate 8 row `KHKK-N{n}` vào cùng cột = gỡ luôn regex | FE lọc gợi ý theo naming-convention, không rào máy |
| Máy rộng UI hẹp (S164) | Trung bình — máy đọc TÊN (regex họ thứ 2; guards file `:28-32` tự cảnh báo "chép lần nữa = 3 bản chuỗi rời nhau, sửa 1 quên 2"; đã có 1 vụ case-collision phải vá 2 tầng) | ĐÚNG NHẤT — quan hệ thành DATA, schema tổng quát phủ cả KHKK lẫn HĐ, UI chỉ bung ở type-3 | SAI — không có máy |
| Chi phí | Thấp: 0-mig, khuôn qua UAT KHKK; NHƯNG bắt buộc kèm gỡ Bom 1 + Bom 2 (§2c) | Trung: mig 3-file + đụng bảng dùng chung 10 module (Designer/AdminFeatures/DTO blast) + vẫn phải gỡ Bom 1/Bom 2 theo chiều cột | ~0 |
| Rủi ro chính | Tên = hợp đồng ngầm; drift 2 họ regex | Blast radius bảng chung; backfill semantics | Pin chéo loại lọt như hôm nay — KHÔNG đạt đề bài |

**Xếp bằng chứng (không quyết thay anh):** PA-C loại. PA-A rẻ nhất, có tiền lệ sống; PA-B đúng "máy rộng" nhất và trả nợ kỹ thuật regex-2-họ. Đường giữa khả thi: **PA-A trước (ship nhanh, 0-mig) + PA-B là bước unify sau** khi 2 họ cùng sống — nhưng chọn A/B/lai đổi chi phí dài hạn ⇒ **CHỜ-ANH #2**.

---

## §3 Q3 — Kế thừa KHKK→HĐ: bridge K7 hiện chảy gì, thiếu gì, pipeline nối thế nào

### 3a. Bridge K7 (commit `aaed6992`, 2026-08-01, 13 file +2241 dòng; BE = controller +34 · Features +23 · bridge 251 · DbInitializer +90 · 3 test-file +1053) — dòng chảy HIỆN TẠI

**Rào nổ trước khi chảy** (`CreateContractFromSigningPlanFeatures.cs`): plan phải `DaDuyet` (`:97-99`) · lines ∈ plan sau Distinct (`:101-109`) · 1 HĐ = 1 NCC (`:111-115`, liên-danh = N lần bấm) · per-line `ApprovedAmount` NOT NULL — CẤM fallback `ProposedAmount` (`:119-127`) · line chưa có HĐ (`:128-129`) · workflow pin 4 vế (`:132-148`).

**Chảy sang HĐ**: SupplierId (line) · ProjectId + DepartmentId (plan `:191-192`) · `GiaTri = Σ ApprovedAmount` đúng tập line chọn (`:173`) · TenHopDong auto "TenGoiThau — hạng mục" clamp 500 (`:175-184`) · NoiDung = pe.MoTa (`:196`) · BudgetManual = "NS tham chiếu KHKK {mã}" + Σ PeReferenceAmount (`:199-200`) · mã HĐ RG-001 gen NGAY (`:212`) · pin V1 active-per-type (`:163-166`,`:201`) + pin V2 user-chọn (`:202`) · SlaDeadline (`:205-206`) · changelog 2 phía (`:216-235`) · **`Line.ContractId` = nguồn thật** (`:238`) + `pe.ContractId ??=` fallback hiển thị có-chiều (`:240-246`).

### 3b. KHÔNG chảy — đây là danh sách gap cho "phiếu duyệt HĐ kế thừa từ KHKK"

1. **`ContractType` chọn TAY trong dialog** (`:53` request param). Danh mục SP-002 KHÔNG mang map → 7 ContractType: `ContractCatalogEntry` chỉ có `GroupCode/ApprovalGroup/SignerRole/InitialsChain` (`ContractCatalogEntry.cs:15-24`). Spec-memory `:17`: hạng mục duyệt xong → *"đưa vào loại hợp đồng gì"* = bước NGƯỜI quyết. ⇒ workflow auto-theo-loại chỉ đúng khi loại đúng, mà loại đang là input tay. **CHỜ-ANH #3**: có thêm cột gợi-ý loại HĐ vào danh mục SP-002 (map catalog→ContractType, autofill dialog) hay giữ chọn tay thuần?
2. **Hạng mục per-line KHÔNG vào ruột HĐ**: bridge không sinh row nào trong 7 bảng Details per-type (`Contract.cs:63-69`) — tên hạng mục chỉ thành chuỗi TenHopDong. GĐ3 = "bàn điều khoản, không bàn hạng mục nữa" (spec-memory `:17`) nên có thể ĐÚNG thiết kế; nhưng phụ lục 3.5 SP-002 (bảng KL/đơn-giá/thành-tiền + VAT) nếu phải render ở GĐ3 thì cần line-level. **CHỜ-ANH #4**.
3. **`ApprovalGroup` (N1-N8) không chảy** — HĐ không giữ nhóm nguồn; nếu CHỜ-ANH #1 chọn trục nhóm (hoặc cần báo cáo theo nhóm xuyên GĐ) thì HĐ cần cột này (1 AddColumn) hoặc đọc ngược qua `Line.ContractId → plan.ApprovalGroup` (0-mig, đắt query).
4. **Dossier/attachment KHKK không copy** — truy ngược được qua Line; hồ sơ GĐ2 đã chốt nên để-nguyên có lý. Ghi nhận, không đề xuất đổi.

### 3c. Pipeline 4-GĐ nối trạng thái — KHÔNG cần phase mới, KHÔNG cần entity phiếu mới

`KHKK DaDuyet` (`ContractSigningPlanPhase.cs:12`) → bấm bridge per-NCC → `Contract DangSoanThao=2` (`:189`) → Drafter/Procurement trình (`ContractWorkflowService.cs:70-102`, vai 4 vế `:84-88`) → `ChoDuyet=10` → duyệt V2 từng Bước×Cấp (`:111-116` → `ApproveV2Async :230`) → `DaPhatHanh=9` terminal (`ContractPhase.cs:24`) → GĐ4 = attachment `SealedCopy` (`ContractFeatures.cs:355` `hasSealedCopy`). **"Phiếu duyệt HĐ" GĐ3 = chính `Contract`** — đã có đủ bộ ruột phiếu (Approvals + LevelOpinions + Changelogs + Comments + Attachments, `Contract.cs:48-58`) tương đương 7 bảng KHKK; dựng entity phiếu mới = chế thêm, phạm luật "GIỐNG NHAU — CHỈ KHÁC FORM — ĐỪNG CHẾ THÊM" (spec-memory `:57-59` + chính bridge tự trích luật này `:17`).

## §4 Q4 — Schema delta TỐI THIỂU

**Đường mặc định (PA-A + "Contract = phiếu GĐ3") = 0 MIGRATION.** Mọi thứ cưỡi máy có sẵn: họ workflow = data-op seeder idempotent per-Code (khuôn `DbInitializer.cs:540-542`) · guard/bomb-fix/re-pin = code thuần · phiếu + ruột phiếu + con-trỏ + LevelOpinions đã có từ Mig 32/33. Ground-truth đo hôm nay (2026-08-10, sqlcmd): **Dev VÀ Design đều đã áp Mig 72** `20260806070242_AddMaHangMucToContractSigningPlanLines` — drift committed-but-unapplied S182 ĐÃ HẾT, nền sạch để build.

**Mig phát sinh CÓ ĐIỀU KIỆN** (mỗi cái 1 AddColumn nhỏ, 3-file rule bắt buộc, Down reversible):
- (a) Nếu chọn **PA-B**: +1 cột phân-nhánh nullable trên `ApprovalWorkflows` (+ backfill optional parse `KHKK-N%` → cột để unify; gotcha #64 — backfill chạy THẬT lần đầu trên prod, SQLite test bỏ qua migration).
- (b) Nếu **CHỜ-ANH #1** chọn trục nhóm / cần báo-cáo nhóm xuyên GĐ: +1 cột `Contract.ApprovalGroup` (khuôn Mig 71: AddColumn + backfill đọc ngược `Line.ContractId → plan.ApprovalGroup`).
- (c) Nếu **CHỜ-ANH #3** duyệt: +1 cột gợi-ý-loại-HĐ trên `ContractCatalogEntries` + cập nhật seed 86 dòng.

Đo hiện trạng Dev làm mốc so sánh: `ApplicableType=3` có ĐÚNG 1 workflow `QT-HD-V2-001` v1 active+selectable (sqlcmd hôm nay) — mọi HĐ V2 hiện đi chung 1 quy trình, khớp chẩn đoán §1b.

---

## §5 Q5 — AUTHZ: hiện trạng verify hôm nay + 3 phương án — 🔴 TOÀN BỘ CHỜ-ANH (#5)

**Hiện trạng (đọc lại nguyên trạng 2026-08-10, không đổi so S156):**
- `ContractsController.cs:13` `[Authorize]` TRẦN class-level; per-action policy DUY NHẤT là `Contracts.Read` trên GET `/deleted` (`:31-32`). **22 endpoint GHI không policy per-action** (đếm grep hôm nay: POST `:49,:64,:71,:88` + 7 POST details `:126-168` + PUT `:56` + 7 PUT details `:182-224` + DELETE `:78,:113,:175`).
- `ContractWorkflowService.cs:48-66`: nhánh **Reject chạy TRƯỚC MỌI GUARD** — không check vai, không check phase ⇒ user đăng nhập bất kỳ Từ-chối/Trả-lại được bất kỳ HĐ nào kể cả `DaPhatHanh` (gotcha #82). Menu HĐ đang public 13/13 role **chủ-đích anh** (STATUS `:23` ⑦-risk: *"cho hiển thị hết để mọi người góp ý"*), anh chốt 07-28 *"chỗ hợp đồng cứ từ từ"* + tái khẳng định @S159.
- Mâu thuẫn mới: GĐ3 = mở luồng GHI Contract cho nhiều người dùng thật ⇒ cái treo cũ giờ nằm ngay giữa module mới. Tiền lệ tốt đã có: KHKK controller 2 tầng (class `KeHoachKyKet.Read` + action `.Create/.Update/.Delete` — `ContractSigningPlansController.cs:15-17,:29-286`) và bridge **hội-2-khóa** class `KeHoachKyKet.Read` ∧ action `Contracts.Create` (`:299-307`, AND-cộng-dồn của ASP.NET). Test khuôn sẵn từ `aaed6992`: `KhkkBridgeAuthzTests.cs` (245 dòng) + `AuthorizePolicyRegressionTests.cs`.

| | Đ1 — per-action policy TRỌN ContractsController (mirror KHKK) | Đ2 — đóng ĐÚNG MẶT TẤN CÔNG, còn lại giữ treo | Đ3 — Đ1 + rà 2-tầng display/API trọn module |
|---|---|---|---|
| Việc | ~24 attribute + grant seeder **code-first** (luật S159/#84: cấm SQL tay) + FE gate đúng key (#85) + extend regression tests | Guard nhánh Reject (đúng-lượt V2/vai + chặn terminal) + policy chỉ cho endpoint MỚI GĐ3 | Đ1 + đối chiếu menu-public-13/13 với grant API từng role (2 tầng độc lập — bài #82) |
| Chi phí | Trung (1 wave riêng) | Thấp nhất | Cao nhất (đè tiến độ GĐ3) |
| Rủi ro | **Silent-403 rải UI** nếu grant hẹp hơn menu-public (#44/#85); probe phải #87-aware (415/411 = PRE-AUTH, không đọc thành authz) | Các endpoint ghi cũ (update/details/attachments) VẪN trần | Như Đ1 + phạm vi UAT lớn |
| Được gì | Đóng #82 dứt điểm cho HĐ | Đóng đúng lỗ nguy hiểm nhất (Reject-any) với ~0 phá vỡ | Trạng thái sạch vĩnh viễn |

**Không tự quyết** — anh từng treo chủ-đích, nay module chạm thẳng: cần anh chọn Đ1/Đ2/Đ3 (hoặc Đ2-trước-Đ1-sau) = **CHỜ-ANH #5**.

---

## §6 Q6 — WAVE BE đề xuất (mỗi wave: deliverable · acceptance ĐO ĐƯỢC · rủi ro)

- **W0 — CHỐT TRỤC (0 code, chặn-đầu):** trả lời CHỜ-ANH #1 (trục 7-type ⟂ 8-nhóm) · #2 (PA-A/PA-B/lộ-trình A→B) · #5 (authz Đ1/Đ2/Đ3) · #6 (roster người duyệt). *Acceptance:* 4 đáp án vào sổ YC (mã vĩnh viễn). *Rủi ro:* bỏ W0 = dựng nhầm trục, đập lại 7 workflow + guard.
- **W1 — MÁY per-loại (code-only, 0-mig nếu PA-A):** gỡ **Bom-1** (deactivate per-Code mở cho type-3, predicate data-driven thay hard-wire `== ContractSigningPlan` tại `ApprovalWorkflowV2AdminFeatures.cs:370`) + **Bom-2** (overview family type-3, `:205-215`) + guard `EnsureContractTypeMatch` 1-nhà-duy-nhất (mirror `ContractSigningPlanWorkflowGuards`). *Acceptance (test-before RED→GREEN, khuôn W3 KHKK):* (i) POST version mới cho code loại X → `IsActive` của code loại khác GIỮ NGUYÊN (đếm SQL trước/sau); (ii) pin chéo loại → 409. *Rủi ro:* đổi predicate deactivate chạm hành vi 8 type khác → chạy trọn regression; SQLite mù collation (`:345-347`) — dựa normalize sẵn có, đừng viết test case-collision vô nghĩa.
- **W2 — SEED họ workflow type-3 (data-op, 0-mig):** mirror `SeedKhkkGroupWorkflowsAsync` per-Code idempotent; roster theo W0-#6; thiếu user → bỏ slot + warning, KHÔNG throw lúc boot (`DbInitializer.cs:544-548`). *Acceptance:* boot ×2 → đúng N row họ mới type-3 `IsActive=1` không nhân đôi; `QT-HD-V2-001` cũ KHÔNG bị đụng active (per-Code). *Rủi ro:* infra-seed UNGATED chạy thẳng prod lần deploy đầu (#51) — roster phải chốt TRƯỚC merge.
- **W3 — WIRE 3+1 cửa pin:** bridge K7 auto-resolve workflow theo `ContractType` (giữ/bỏ override tay theo CHỜ-ANH #7) + Create bổ 2 vế `IsActive ∧ IsUserSelectable` (đồng luật vá-3, xóa drift §1c-2) + **cửa re-pin mới** trên UpdateDraft (§1c-3) + guard khớp-loại tại cửa TRÌNH trong `ContractWorkflowService` (site Infrastructure — đúng lý do guards là `public static`, `ContractSigningPlanWorkflowGuards.cs:14-22`). *Acceptance:* phiếu thầu-phụ pin quy trình nguyên-tắc → 409 ở CẢ 4 cửa; HĐ sinh từ bridge assert đúng Code họ theo type. *Rủi ro:* đổi contract API dialog → FE 2 app phải theo (ngoài lăng kính BE — khai để lead điều phối lane FE).
- **W4 — DRY-RUN toàn trình + nghiệm thu:** khuôn K8 S168, 2 nhánh 2 loại HĐ khác nhau → 2 quy trình khác nhau, đi trọn PE→KHKK→bridge→duyệt V2→`DaPhatHanh`→SealedCopy trên Dev; đo inbox approver cấp-đang-chờ (ResolveV2InboxIds). *Acceptance:* sổ ID phiếu + seq cháy ghi lại (bài K8: phiếu test làm lệch số phiếu thật); probe authz #87-aware. *Rủi ro:* đọc nhầm 415/411 thành kết luận authz.
- **W5 — AUTHZ thi hành theo W0-#5** (điều kiện anh chốt): policy + grant seeder code-first + extend `AuthorizePolicyRegressionTests` + FE gate đúng key (#85). *Acceptance:* user thường Reject HĐ `DaPhatHanh` → bị chặn (control dương: admin vẫn thao tác được); regression policy xanh. *Rủi ro:* 403-storm nếu grant thiếu role — grant-before-tighten.

---

## §7 Surprises (ngoài câu hỏi chính) + CHỜ-ANH tổng hợp

**Surprises:**
1. **Brief lệch mốc mig:** đĩa + `docs/STATUS.md:468` + Dev + Design đều = **Mig 72** (brief ghi 71). Drift local S182 đã hết.
2. **S156 STALE:** "approver V2 mù HĐ ChoDuyet" đã VÁ @W6 S161 (inbox/list/detail V2-aware) — diary investigator cần đính chính.
3. **Vá S182 chỉ phủ type-10:** bất biến "1 active/type" vẫn khắc chữ cho mọi type khác (`ApprovalWorkflowV2AdminFeatures.cs:363`) — GĐ3 mirror họ-Code sẽ giẫm ĐÚNG quả bom đó nếu quên W1.
4. **Dev type-3 = đúng 1 workflow** `QT-HD-V2-001` (đo SQL) — "quy trình theo loại" hiện = 0% tồn tại ở V2.
5. **Contract KHÔNG có cửa re-pin** (`UpdateContractDraftCommand` `ContractFeatures.cs:136-145` thiếu field) — pin nhầm là kẹt, sẽ cắn ngay khi có guard per-loại.
6. **Drift 1-luật-2-site đang sống:** guard pin Create thiếu 2 vế mà bridge có (`:79-87` vs `CreateContractFromSigningPlanFeatures.cs:141-148`) — đúng bug-class W1b S182.
7. (nhỏ) Dev không có workflow type-1/2 (PE) — 0 row; dry-run W4 trên Dev phải tạo workflow PE trước.

**CHỜ-ANH (7 câu, không đoán):**
- **#1** Trục quy trình GĐ3: 7 `ContractType` (chữ YC-023) ⟂ 8 nhóm SP-002 (chuỗi ký nháy đi theo trục này) ⟂ lai?
- **#2** PA-A (họ-Code 0-mig, tiền lệ KHKK) hay PA-B (cột thật, "máy rộng" đúng nghĩa) hay A-trước-B-sau?
- **#3** Danh mục SP-002 có nên mang cột gợi-ý loại HĐ (autofill dialog bridge) hay giữ chọn tay?
- **#4** GĐ3 có cần line-level (bảng KL/đơn-giá/thành-tiền phụ lục 3.5) trong ruột HĐ, hay chỉ điều khoản (hiện trạng: hạng mục chỉ thành chuỗi tên)?
- **#5** Authz Đ1/Đ2/Đ3 — anh từng treo *"chỗ hợp đồng cứ từ từ"*, module này chạm thẳng.
- **#6** Roster người duyệt cho họ workflow type-3: sao từ đâu? (KHKK sao từ `QT-DN-V2-001` theo OG-2; SP-002 pattern nháy per-NHÓM không per-loại ⇒ không tự suy được).
- **#7** UX bridge: auto-pin theo loại có cho override chọn tay không?

**Token cost estimate:** ~95K (đọc code + spec + đo DB + ghi file).

== HẾT ==
