# SPEC CỤM-3 FINAL — K7 + K8 (lead kết tinh sau B2-r cụm-3, @S164)

## ① Đề bài
K7 cầu KHKK→HĐ (bridge 8v/8test — đường ống S156 ĐÃ vá @W6-S161, verified 3 chân) · K8 dry-run E2E 17 bước. NỀN = `sub-invest-fable-b2-cum3.md` (22.9KB; L1 chấm **7/7 claim ĐÚNG**) — **16 VÁ dưới thắng khi xung đột** (từ `sub-reviewer-c3-l{1,2,3}.md`: L1 PWF 4M · L2 FAIL 5H · L3 FAIL 4H).

## ② 16 vá

**Trục AUTHZ bridge (3-lane hội tụ — nặng nhất):**
1. 🔴 **[L1-F2 ⨯ L2-H1 ⨯ L3-H1/H2] Gate nút bridge = HỘI 2 policy** (`KeHoachKyKet.Read` class-level ∧ `Contracts.Create` action-level) — spec/FE phải khai ĐỦ 2 vế; và 🔴 **`Contracts.Create` hiện CHƯA CẤP vai nào** (seeder `CanCreate=isPe` ⇒ false cho `Contracts`) ⇒ thêm việc: **grant `Contracts.Create` cho vai được bấm** (đề xuất: Drafter + Procurement + Admin — [chờ câu III anh; default người-có-quyền-tạo-HĐ]) + **test authz 2 chiều** (vai có quyền → 201 · vai thiếu 1 trong 2 vế → 403) + FE gate đúng hội-2-khóa (không lỏng hơn server). Ghi chú: endpoint `POST /api/contracts` hiện `[Authorize]` trần (#82 treo chủ-đích — KHÔNG sửa đợt này, chỉ khai).
2. **[L1-F9] B0 pre-flight picker đủ 2 vế**: workflow type-3 phải `IsActive` **∧ `IsUserSelectable`** (dropdown lọc cả 2 — `ContractCreatePage.tsx:334-341`); B0 thêm SQL check cả 2 cờ kẻo dialog rỗng.
3. **[L1-F11] Guard bridge re-check `IsActive`/`IsUserSelectable`** của workflow được pin (khuôn `ContractFeatures.cs:74-88` chỉ check tồn-tại+type — forge POST pin được quy trình retired; thêm 2 điều kiện, rẻ).

**Trục dữ liệu/cây:**
4. 🔴 **[L1-F6] `pe.ContractId ??=` có chiều NGƯỢC** — bridge-PE-cũ chạy trước thì `??=` no-op im lặng ⇒ HĐ mới **vô hình trên cây** (cây đi qua `pe.contractId` ĐƠN `usePipelineStages.ts:214-218`). Vá 2 tầng: (a) K7 ghi `Line.ContractId` luôn (nguồn thật) + `pe.ContractId ??=` chỉ là fallback hiển thị; (b) **K5-liên-đới: builder GĐ3/GĐ4 nối qua TẬP `lines[].contractId`** của các phiếu KHKK thuộc gói (∪ `pe.contractId` legacy) — không thì OG-1 UI-1-1 (N HĐ/PE) chỉ hiện HĐ đầu.
5. **[L3-H3] T2 (gộp 2 line cùng NCC) phụ thuộc Mig 71 DROP-unique** — thêm vào danh sách phụ-thuộc K7←K2 (hiện sót); test T2 chạy SAU K2.
6. **[L1-F7] Bridge bổ sung theo tiền lệ**: nạp `Project.Code` + `Supplier.Code` (+2 guard NotFound, khuôn bridge-cũ `:63-66/:85-86`) cho codegen `GenerateAsync` · field-map += `SlaDeadline` · **`WorkflowDefinitionId` pin như 2 tiền lệ** (`ContractFeatures.cs:105-109` "Cả 2 set ok") — không để null rơi về policy hardcode `:647-662`.
7. **[L1-F10] Acceptance B6 đổi neo**: `EndedByLevelFinalize` phía KHKK là vật K3 MỚI ĐẺ (hiện 0 hit) — B6 neo vào {phase DaDuyet + `ApprovedAmount` frozen per-line + K3-cột-runtime} tùy nhánh; khai rõ bảng nào cột nào.
8. **[L1-F8] Sửa nhãn "7 case" → 8** (bảng T1-T8).

**Trục K8 dry-run (L2 — "chưa chạy được như viết"):**
9. 🔴 **[L2-H5] B0 = FORM NGƯỜI THẬT fail-closed**: bảng 14 dòng {bước · người · email · tài khoản} PHẢI ĐIỀN ĐỦ trước giờ-G (12/14 bước đang trống); nguồn = K3-acceptance LIVE; thiếu 1 dòng = KHÔNG chạy.
10. 🔴 **[L2-H2] Phép đo QĐ9 phải TRƯỢT ĐƯỢC**: sau finalize → **sửa NS/giá nguồn** (PE hoặc catalog) → reload phiếu → số **KHÔNG đổi** = PASS (đổi = FAIL); đo **CẢ 2 nhánh** (a-Chương finalize-CCM VÀ lên-CEO-Trường duyệt) — hiện phép đo 0-bit.
11. 🔴 **[L2-H3] Dấu ZZTEST cho phiếu KHKK**: mã phiếu auto-gen (`KHKK/2026/xxx`) KHÔNG mang ZZTEST — bản ghi sống-vĩnh-viễn lại không dấu. Vá: nhét `ZZTEST` vào field hiển thị được trên list (tên kế hoạch/ghi chú — soi DTO chọn field) + **rollback theo DANH SÁCH ID** ghi lại từng bước, không theo prefix.
12. ✅ **[CHỐT @S164 — câu V] "Phiếu đã duyệt không xóa" = THIẾT KẾ ĐÚNG, owner ratified** (verbatim: *"đồng ý luôn, hiện tất cả các phiếu nếu đã duyệt gần như ko xóa đc. Đồng ý cái này đúng với thiết kế ERP."*) ⇒ (i) delete allow-list `{DangSoanThao, TuChoi}` **GIỮ NGUYÊN — CẤM B4 "sửa" nó** (nó không phải bug); (ii) hệ quả vận hành: phiếu test đã duyệt BẤT TỬ ⇒ **K8 CHỐT dùng bộ PE TEST RIÊNG — owner ratified @S164** (verbatim: *"tạo 1 cái PE Test SOLUTION-PE-1, 2, 3 j đó để test, cái này đúng ko test trên phiếu thật"*): dựng **gói thầu/hạng mục test đặt tên `SOLUTION-PE-1..n`** (dưới dự án test — tái dùng convention ZZTEST-project S160 hoặc dự án test sẵn có), phiếu PE tạo trên gói đó đi trọn vòng từ GĐ1; ⚠️ note kỹ thuật: MÃ phiếu vẫn auto-gen theo RG-001 (không nhét chữ test vào mã được) — nhận diện test qua TÊN GÓI `SOLUTION-PE-{n}` hiện trên list + cây, và danh-sách-ID ghi lại từng bước (vá-11); (iii) 🆕 **BACKLOG giai-đoạn-sau (lời anh):** tính năng **"phiếu điều chỉnh/thay thế"** — cập nhật chứng từ đã duyệt bằng chứng từ điều chỉnh, KHÔNG xóa gốc (khuôn amendment ERP; áp được cả PE/KHKK/HĐ) — ghi nhận, KHÔNG làm đợt này.
13. **[L3-H4] B16 rollback HĐ = NO-OP hiện tại** (numeric guard chặn xóa cả khi TuChoi) — rollback plan ghi rõ: HĐ test dọn bằng **sqlcmd soft-delete** (lệnh cụ thể per bảng: Contracts + ContractApprovals + Changelogs + Line.ContractId=NULL + pe.ContractId=NULL), thứ tự ngược.
14. **[L2-M10] Câu III treo → acceptance "ai bấm" để [CHỜ-ANH]** — không chốt trong DoD tới khi anh trả; default dialog + người-có-quyền.
15. **[L2-M12] DoD cardinality đo được**: sau bridge UI-1-1 — đếm `SELECT COUNT(DISTINCT ContractId) FROM Lines WHERE PlanId=X AND ContractId IS NOT NULL` == số lần bấm; gộp (sau này) = N line 1 ContractId.
16. **[L3-Q5] Dialog bridge tái dùng khuôn sẵn** (dialog confirm shadcn/ui đã dùng ở Xóa-phiếu S155 + select khuôn `ContractCreatePage` picker) — 0 khuôn mới (lời 9).

## ③ Checklist final cụm-3
- [ ] K7 (SAU cụm-1 K2+K3): bridge 8v nền + vá 1-8 · test 8+2-authz+idempotency-double-click · acceptance 7 nền + hội-2-khóa 403-path + cây hiện HĐ qua lines[].contractId + `giaTri == Σ ApprovedAmount` line chọn (null → message) · baseline ≥590+k.
- [ ] K8 (CUỐI, sau MỌI wave land + câu V anh chốt): B0-FORM 14 người fail-closed + pre-flight SQL (workflow LIVE 2-cờ + KHKK-N4 + catalog N-row) · 17 bước mỗi bước {người thật · hành động · kỳ vọng · chụp} · QĐ9 đo 2 nhánh trượt-được · ZZTEST field-hiển-thị + rollback theo ID (sqlcmd cả HĐ lẫn phiếu) · notification dặn team trước.

END spec-cum3-final — nền 7/7-claim-đúng + 16 vá · authz-hội-2-khóa đóng · K8 từ "kịch bản" thành "chạy được".
