# SPEC TỔNG QUÁT v2 — Chuỗi 4 GĐ theo SOL-CCM-SP-002 (B1a + B1b review, lead kết tinh @S164) > v1 = kết tinh từ `sub-invest-fable-b1a.md` (spot-check 4/4). **v2 = v1 + 14 vá từ B1b** (`b1b-review-synthesis.md` — 3 lane reviewer 41 finding thô; lens2 xác nhận 6/6 claim chịu-lực ĐÚNG ⇒ nền giữ, vỏ vá). Yêu cầu gốc: `draft-yeu-cau-owner.md`. ## ① Tính năng / đề bài Chuỗi 4 GĐ per-gói-thầu theo SOL-CCM-SP-002: GĐ1 Duyệt NCC (PE = CHA, đã chạy) → GĐ2 KHKK = **duyệt HẠNG MỤC theo 8 NHÓM DUYỆT** (trình ký số trạm-by-trạm y Duyệt NCC; ký nháy = mực trên bản cứng GĐ4) → GĐ3 điều khoản → GĐ4 bản cứng 1-mốc. Scope 5 mảnh: master-data danh mục · sidebar 8 nhóm × 6 leaf · cây mirror sidebar · admin thu gọn + 8 designer · dry-run E2E. 🔴 Luật khuôn: **GIỐNG NHAU — CHỈ KHÁC FORM — ĐỪNG CHẾ THÊM**. 🔸 Khai thẳng (B1b l3-F4): đợt này **8 nhóm = đơn vị HIỂN THỊ + luồng duyệt**, KHÔNG phải đơn vị authz — mọi endpoint KHKK giữ 1 bộ policy `KeHoachKyKet.{R,C,U,D}` per-action (`ContractSigningPlansController.cs:28+`); authz-per-nhóm nếu anh muốn = wave riêng sau. ## ② Cách implement — 9 quyết định (v2; B3-A0b sửa đếm) 1. **[OG-1]** Cardinality "đưa vào loại HĐ" default **GỘP theo cặp (NCC × dòng-danh-mục)** — N line chung `ContractId` (hợp khuôn `ContractSigningPlanLine.cs:25`, index không-unique `Configuration:26`); đảo 1-1 được cùng máy. 2. Phiếu KHKK = **per NHÓM** (1 phiếu = 1 nhóm, N hạng mục). Lines per-hạng-mục: Mig +`CatalogEntryId Guid?` +`TenHangMuc`, UNIQUE → (PlanId, SupplierId, CatalogEntryId) filtered. 🔴 **+2 vá B1b:** (a) **đổi rào 1-PE-1-phiếu** (`ContractSigningPlanFeatures.cs:342-347` + picker `:1201-1202`) → (PeId, ApprovalGroup) **[OG-7]** — FE tree đã chịu N-phiếu/PE (`usePipelineStages.ts:110-121`); (b) **bất-biến tiền**: `PeReferenceAmount` = snapshot per-NCC-toàn-phiếu (`Line.cs:6,:16-20`) — repurpose per-hạng-mục PHẢI định nghĩa lại semantics + Σ trước == Σ sau mig, acceptance rà **18 consumer** (17 grep-verified cụm-1 + `Features.cs:712-713` Σ Proposed — số chốt theo cụm-1 vá-8; B3-A-x). 3. Workflow = **1 type-10 + 8 Code `KHKK-N1..N8`**; only-ONE-active VÔ HẠI (chứng 2 mắt xích l2: create chỉ check type `:209-220`; overview không lọc IsActive `:117-134`, POST không đụng IsUserSelectable `:341-353`). 🔴 Cột `ApprovalGroup` **K2 là CHỦ DUY NHẤT** (l3-F6) — K3 chỉ đọc (map Code↔group + validator phiếu-pin-đúng-nhóm). 4. Vai catalog = 2 tầng: danh mục lưu chuỗi vai TEXT (in khối ký GĐ4) ⟂ workflow Levels lưu NGƯỜI THẬT. 🔸 Khai limitation (l1-F12): TEXT không ràng buộc máy với người-duyệt-thật — chấp nhận đợt này, khai trong doc K1. 5. NĐUQ = cột THAM KHẢO text **[OG-3 nâng cấp]**; enforce máy = **KHÔNG BAO GIỜ** (human-gate vĩnh viễn — a Chương quyết; B3-A0a: xóa nốt vế "wave opt-in sau" sót ở đây). 6. Menu: GIỮ `Khkk_G1` + 6 leaf cũ, đổi LABEL = nhóm 1 (labelBackfill khuôn S155). Nhóm 2-8 = **42 leaf MỚI + 7 group-key mới (49 row menu mới; 48 leaf TỔNG)** — sửa số l1-F8/l2-F5. 🔴 Phiếu cũ: **backfill `ApprovalGroup = 1`** (l3-F5 — mirror bài `EvaluationType` không rỗng; acceptance đếm `COUNT(phiếu) == Σ COUNT 8 folder`). 7. Admin ẩn = fe-admin **`isAdminHidden` MỞ RỘNG TẬP KEY TƯỜNG MINH** (giữ blacklist — KHÔNG đảo whitelist, tránh đổi default menu-mới; vá hội tụ l1-F2 ⨯ l3-F1/F2): ẩn thêm {`Dashboard?`→giữ, các root nghiệp vụ: `DuyetNcc-root keys`, `KeHoachKyKet`, HR/Office/Personal, `Contracts`(đã ẩn `Ct_*`), `HardCopies`, `Forms`, `Reports`, `Catalogs/Master`, **`Workflows` (:1795)**, **`PeWorkflows` (:1798)**} — CHỪA {`System` + 4 leaf quản trị + **`ApprovalWorkflowsV2` (:1801, con System)** + subtree}. Danh sách key CHÍNH XÁC = việc B2-cụm-2 liệt từ `DbInitializer` seed; acceptance = **liệt kê đúng N mục lá còn thấy** (l3-F2). 8. Nguồn danh mục: **file đĩa `QUY_TRINH/QT TRINH KY HOP DONG TP-NCC.docx` = ĐÚNG SOL-CCM-SP-002.v01 (mã ở word/header1.xml) NHƯNG bản 6-trang KHÔNG chứa bảng** (lead đo). Nguồn bảng = **7 ảnh anh gửi @S164 (trang 5-9, trọn 5.1 A/B/C + 5.2 CEO)** → transcribe → **[OG-6] anh soát bảng seed trước land** (hoặc anh gửi file 15-trang). "/" trong ô vai = OR-of-N. 9. 🆕 **KHÓA BUDGET TẠI FINALIZE (owner @S164; văn SỬA theo B3-A4 — thiết kế THẬT do cụm-1/cụm-3 chốt, cha theo con):** khi phiếu KHKK **finalize tại CCM (a Chương) HOẶC CEO duyệt final** ⇒ số KHÓA. 🔴 **KHÔNG chép khuôn 11-cột Mig 67** — máy freeze per-line ĐÃ CÓ SẴN: `PeReferenceAmount` copy-at-create (`Line.cs:17-20`) + `ApprovedAmount ??=` tại choke-point (`WorkflowService.cs:301-305`, 1-site-DaDuyet verified `:129/:278/:289-292`) ⇒ việc còn lại = **phase-guard line-edit sau DaDuyet (K2) + test-khóa 1-site (K2) + phép đo 2-nhánh TRƯỢT-ĐƯỢC (K8 — sửa nguồn → reload → số KHÔNG đổi, đo cả nhánh CCM-finalize lẫn CEO)** + cột runtime `EndedByLevelFinalize` (K2 item-6 đẻ, K3 tiêu thụ — 3 spec cùng 1 cột ✓). Người quyết = **TP.CCM Phan Văn Chương**. Né #81-EXT: sweep site gán Phase — đã liệt 4 site, GỠ nghi án `WorkflowService.cs:342` (comment đọc-đủ-khối: CỐ Ý không có site gán-qua-biến). ## ③ Wave K1-K8 (v2 — PRE-gate + acceptance có răng) **Cụm 1 (nền):** - **K1 Master-data danh mục** — PRE: **[OG-6]**. BE entity+config+CQRS+controller per-action policy; Mig 70 (3-file rule) filtered-unique Code #57; seed per-Code idempotent; FE leaf Catalogs ×2 app. DoD: N dòng khớp **~85±5, lệch giải trình từng dòng** (đối chứng ảnh) · tên VN+**EN** đủ · CRUD 200 · seed ×2 không nhân row · test **≥590+k, khai k**. - **K2 Phiếu mang hạng-mục + nhóm** — PRE: **[OG-1] + [OG-7]**. Mig 71 (3-file): plan +`ApprovalGroup int` **backfill=1**; Lines +2 cột + đổi UNIQUE; **đổi rào (PeId)→(PeId,ApprovalGroup) 2 site** `:342-347`/`:1201-1202`; list-query +param `group` (`ListContractSigningPlansQuery` `:622-625` — **K2 chủ**, l3-F3) + DTO `ContractSigningPlanListItemDto:34` +`approvalGroup` (sửa citation l2-F4) + FE types. DoD: phiếu nhóm 2 CÙNG PE → **201** · line khác nhóm → 409 · phiếu cũ ở folder nhóm 1, `COUNT == Σ 8 folder` · **Σ tiền trước == sau mig** · bảng rà 16 consumer ĐÃ-XỬ/KHÔNG-ĐỔI · test ≥590+k. - **K3 8 workflow nhóm + designer** — PRE: **[OG-2] gật roster + verify đội hình LIVE (không phải archived)**. Seed 8 workflow `KHKK-N1..N8` type-10 (khuôn `:171`) + validator pin-đúng-nhóm; **8 menu-con dưới `ApprovalWorkflowsV2`** + mở `resolvePath` fe-admin `Layout.tsx:166-172` nhận code mới (leaf lạ rớt im lặng #50) + route designer per-workflow. DoD: API trả 8 · **sidebar admin đếm được 8 designer, mở designer nhóm n load đúng `KHKK-N{n}`** (l3-F10) · phiếu nhóm 3 pin workflow nhóm 5 → 409 · duyệt trọn trạm 1 phiếu → DaDuyet · test ≥590+k. **Cụm 2 (UI):** *(l3-F8: tách phụ thuộc thật — K4a chạy song song cụm 1 được)* - **K4a Menu+grant** (0 phụ thuộc): 49 row menu + grant 3 SITE (`:1780` seed · `SeedKeHoachKyKetAccessAsync:2415` · **`KhkkKeys():2185` — site-3 l2-F6**, comment `:2204` "quên 1 bên = quyền lệch"). **K4b 4 leaf list + Thao tác** (chờ K2 param). **K4c leaf WfView/Create** (chờ K3): WfView mang `?group=` lọc theo workflow Code nhóm (hiện `WorkflowMatrixViewPage:52-71` chỉ lọc type — l2-F3). DoD: 48 leaf render đúng · leaf lọc đúng group **kể cả WfView/Create** · **2 chiều theo bảng kỳ-vọng 13 role** (seeder hiện phủ 7 vai `:2671-2675` — 6 vai còn lại khai kỳ vọng TRƯỚC, l3-F13) · key cũ 0 mồ côi. - **K5 Cây sub-folder**: GĐ2 group-by `approvalGroup` · GĐ3/4 group-by `c.type` (đã có `:240`); thêm tầng `groups` vào `PipelineStage` (mở rộng khuôn — bẫy `content` phải truyền `count` tay `:74-75`). DoD: sub-folder khớp sidebar · **4 file pipeline SHA-identical ×2 app** (đối chiếu cụm-2 — usePipelineStages · PipelineStageFolders · PipelineTreePanel · trang nhúng; B3 sửa đếm) · build ×2 PASS. - **K6 Admin thu gọn** (độc lập NHƯNG l3-F9: kéo sớm TRƯỚC K3 thì 8 designer chưa tồn tại — nếu kéo sớm, acceptance "đếm 8 designer" dời sang sau K3): QĐ7 tập-key tường minh. DoD: **liệt kê đúng N mục lá còn thấy (đủ tên, dư/thiếu 1 = FAIL)** · fe-user KHÔNG đổi = **`git diff --stat .. -- fe-user/src` = 0 file** (KHÔNG đo bundle-hash #69 — l3-F12). **Cụm 3 (nối + chạy):** - **K7 Cầu KHKK→HĐ** — RE-ĐO **3 NẤC** (l3-F11): (a) cột/index ✓ có (`Line.cs:25`+`Config:26`); (b) **write-site gán `ContractId` = 0 hiện tại** (lead xác nhận grep); (c) test E2E — ⇒ trạng thái NỬA-LAND, K7 build phần (b)+(c). Bridge đọc `ApprovedAmount` per-Line + gộp theo OG-1 + pin workflow V2 + chọn `ContractType` lúc bắc cầu (**ai chọn = câu nghiệp vụ, mặc định người bấm chọn trong dialog — l1-F15**). DoD: KHKK DaDuyet → HĐ đúng số lượng OG-1 · `giaTri == Σ ApprovedAmount` **phạm-vi-gộp khai rõ, null → chặn với message** (l1-F6) · approver V2 thấy + inbox · test bridge ≥590+k. - **K8 DRY-RUN E2E** từ `PE/2026/A/049`. DoD (+3 dòng l3-F16): mọi bản ghi mới mang tiền tố **ZZTEST** · danh sách bản-ghi-không-xoá-được + cách sống chung (HĐ DaPhatHanh guard numeric) · **tắt/dặn-team notification trước khi chạy** · toàn trình hết trạm → HĐ · cây đủ 4 GĐ · screenshot + mã phiếu thật. **Mọi wave:** cicd PASS; **byte-verify #77 CHỈ wave có FE-ship** — wave BE-only = smoke API (l3-F17); mig = 3-file rule. ## ④ [OWNER-GATE] — 8 hàng, cập nhật sống (B3-A0b sửa đếm) | OG | Câu hỏi | Trạng thái | Chặn wave | |---|---|---|---| | OG-1 | Gộp HĐ | ✅ **CHỐT @S164** (verbatim: *"thiết kế gộp nhưng giao diện thì cho 1-1 (Khi user đổi ý thì mình làm luôn)"*) ⇒ **schema GỘP-capable (N line chung ContractId) + UI default 1-1** (bridge = 1 hàm nhận list-line, UI truyền 1 line; bung gộp = FE sau, 0 đổi máy) | K2, K7 | | OG-2 | Roster 8 workflow | ✅ **CHỐT @S164** (*"Đúng đội hình cũ"*) — map đội hình `QT-DN-V2-001`, **verify bản LIVE (không archived) trước seed** | K3 | | OG-7 | 1 PE → N phiếu | ✅ **CHỐT @S164** (*"PE cho nhiều, nhưng… rào hiển thị 1-1 lại — User cần thì bung ra"*) ⇒ **BE mở N-phiếu/PE (đổi rào 2 site :342/:1201 → (PeId, ApprovalGroup)) + UI default hướng-1**: PE có phiếu rồi → hiện phiếu + nút phụ "Tạo thêm cho nhóm khác" (thay 409 chặn hẳn) | K2 | | OG-3 | NĐUQ | ✅ **CHỐT @S164** (*"Đồng ý. Vẫn như trước - các quyền kiểu chuyển CEO các thứ y chang vậy luôn, thì ko cần quan tâm mức duyệt, vì a Chương sẽ quyết định là cái đó cần CEO ko hay là anh ấy đc duyệt."*) ⇒ NĐUQ = ghi chú tham khảo; **cơ chế lên-CEO = TÁI DÙNG level-finalize sẵn có của V2** (người trạm CCM — Phan Văn Chương — tự chọn "kết thúc tại cấp" hay trình tiếp, y PE đang chạy: `EndedByLevelFinalize` runtime + checkbox opt-in S97) — 🔄 **SỬA @B2c1 (mâu thuẫn spec↔code, evidence `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:151-152` `KHKK_EARLY_FINALIZE_NOTE` "backend chưa xử lý đường duyệt sớm"):** cơ chế = tái dùng KHUÔN level-finalize NHƯNG BE KHKK **CHƯA port nhánh** ⇒ K3 có **3 việc code**: (a) port nhánh finalize vào `ContractSigningPlanWorkflowService` (b) cột runtime `EndedByLevelFinalize` né #78 (forward-provision vào Mig 71 ⇒ K3 0-mig) (c) gỡ khóa 2 ô Designer FE. Seed `AllowApproverFinalize=true` cấp CCM-Chương CHỈ SAU khi (a) land — kẻo config-lie #78 | — | | OG-6 | Soát bảng transcribe 86 dòng (gõ tay từ ảnh; 4 ô ⚠️ cuối file) HOẶC anh gửi file 15-trang | 🔄 **ĐỔI NHÃN soát-SAU @PAUSE-2 (mandate chạy-1-mạch)** — bảng admin-editable qua Admin CRUD; seeder skip-if-exists nên sửa-tay CRUD **sống qua restart** (update-branch sẽ đè mất sửa của anh — cố ý KHÔNG có); 4 ô ⚠️ chờ anh soát SAU trên UI | — (HẾT chặn K1) | | OG-8 | Authz per-nhóm | ✅ **CHỐT @S164** (*"Đúng chính xác. Cho hiển thị, phân quyền tính sau."*) — đợt này 8 nhóm = hiển thị + luồng duyệt; XEM/TẠO chung bộ khóa module; authz-per-nhóm = wave sau khi anh gọi | — | | OG-5 | Extract mơ hồ → hỏi khi gặp | mở | K1 | | OG-9 | Bật lại duyệt-sớm KHKK = ĐẢO phán quyết "PHÁ VỠ" @S155 | ✅ **CHỐT @S164** (*"đúng, chỗ budget chốt chặn là a Chương, anh ấy sẽ quyết cái j cần quyền CEO và cái j anh ấy đc duyệt"*) — K3 việc (a)-(e) HẾT CHẶN: port nhánh finalize + cột runtime + gỡ khóa FE + sweep ép-false ≥4 site + sửa comment/panel/HANDOFF khai supersede-S155 | K3 mở | > 🔴 **OG-3 NÂNG CẤP @S164 (cùng lời chốt OG-9):** *"vậy bài toán trần budget xem như ĐÓNG. Vì người chốt chặn budget là anh Chương."* ⇒ NĐUQ/ngưỡng tiền = **HUMAN-GATE VĨNH VIỄN** (TP.CCM Phan Văn Chương quyết cái gì cần CEO) — **XÓA ý "enforce máy = wave opt-in sau"** khỏi mọi spec; cột NĐUQ trên danh mục thuần ghi chú. Không còn wave threshold nào trong backlog. **BACKLOG giai-đoạn-sau (owner @S164, không làm đợt này):** (1) authz-per-nhóm KHKK (OG-8) · (2) **phiếu điều chỉnh/thay thế** cho chứng từ đã duyệt — khuôn amendment ERP, áp PE/KHKK/HĐ (*"Nếu xóa thì có thể làm 1 tính năng điều chỉnh phiếu để cập nhật điều chỉnh/thay thế - ko xóa phiếu đã duyệt. Cái này để giai đoạn sau."*) · (3) bung-gộp UI (OG-1/OG-7 nửa sau — máy đã sẵn). END spec v2 — 8 wave · 9 quyết định · OG đủ · backlog 3 mục giai-đoạn-sau.