# INVEST-BE — KHKK 8 nhóm workflow admin mirror DuyetNcc (S182, Fable deep-pass)

> Trạng thái: ĐANG ĐO — đổ ruột liên tục. Mỗi claim kèm file:line.

## §1 Endpoint map trang approval-workflows-v2 (admin)

**Controller duy nhất:** `src/Backend/SolutionErp.Api/Controllers/ApprovalWorkflowsV2Controller.cs` — route `api/approval-workflows-v2` (:17), class-level `[Authorize]` trần (:18).

| Action | Verb+Route | Authz | file:line |
|---|---|---|---|
| Overview (list all types + versions + history) | `GET ?applicableType=&isUserSelectable=` | **authenticated-only, 0 policy** (CỐ Ý — Drafter cần list để pick lúc create, comment :10-15) | :21-26 |
| Tạo version mới | `POST` | `Workflows.Create` | :28-34 |
| Sửa-tại-chỗ (giữ Id/Version/IsActive) | `PUT {id}` | `Workflows.Create` | :45-51 |
| Ghim/Bỏ ghim (`IsUserSelectable`) | `PATCH {id}/user-selectable` | `Workflows.Create` | :56-62 |
| Xoá version | `DELETE {id}` | `Workflows.Create` | :64-70 |

**Tham số phân loại = `ApplicableType` (int enum), KHÔNG code-prefix.** Enum `ApprovalWorkflowApplicableType` @ `Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:53-71`: DuyetNcc=1 · DuyetNccPhuongAn=2 · Contract=3 · Proposal/Leave/Ot/Vehicle/ItTicket/Travel=4-9 · **ContractSigningPlan=10** (:70). Trang DuyetNcc = `?applicableType=1`; "NCC và Giải pháp" = 2; KHKK = 10.

**Handler Overview:** `Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` — filter generic `d.ApplicableType == typeEnum` (:123-127) + `IsUserSelectable` (:129-132); khi `applicableType=null` group theo `Enum.GetValues<>()` toàn bộ (:199-213) ⇒ **GENERIC, 0 hardcode chặn type-10**. Label map `AwLabels.Type` :90-98 CÓ entry type-10 "Kế hoạch ký kết HĐ" (:97) — nhưng label này là 1-per-type, KHÔNG phân biệt 8 nhóm N1-N8 (8 nhóm cùng type=10 sẽ gộp chung 1 `AwTypeSummaryDto` với 1 `Active` duy nhất — xem §2 hệ quả).

**9 checkbox = 9 cột `bool` per-Level row** trên `ApprovalWorkflowLevel` (`ApprovalWorkflow.cs:99-176`) — schema DÙNG CHUNG mọi ApplicableType:
| # | Checkbox UI | Flag | line |
|---|---|---|---|
| 1 | Trả về 1 Cấp trước | `AllowReturnOneLevel` (F1m1) | :120 |
| 2 | Trả về 1 Bước trước | `AllowReturnOneStep` (F1m2) | :123 |
| 3 | Trả về Người chỉ định | `AllowReturnToAssignee` (F1m3) | :126 |
| 4 | Trả về Drafter (default) | `AllowReturnToDrafter` (F1m4, default TRUE) | :129 |
| 5 | Sửa Section 2 lúc duyệt | `AllowApproverEditDetails` (F3) | :133 |
| 6 | Sửa Section ngân sách | `AllowApproverEditBudget` (F4) | :139 |
| 7 | Duyệt thẳng Cấp cuối | `AllowApproverSkipToFinal` (F2) | :147 |
| 8 | Duyệt là KẾT THÚC (không CEO) | `AllowApproverFinalize` (F5) | :158 |
| 9 | NV XÓA phiếu khi đến lượt | `AllowApproverDelete` (F6, Mig 68) | :173 |

**🔴 Create handler ĐÃ CÓ nhánh type-10 đích danh** (`ApprovalWorkflowV2AdminFeatures.cs:387`): `AllowApproverSkipToFinal = typeEnum != ContractSigningPlan && l.AllowApproverSkipToFinal` — **ép-false F2 CHO RIÊNG type-10** (K3-test O-1: KHKK service 0 lần đọc cờ này, echo true = config-lie #78). `AllowApproverFinalize` từng ép-false @S161 nhưng ĐÃ GỠ @S165 (service KHKK port level-finalize) — comment :376-386.

**PUT sửa-tại-chỗ (S155 F-1):** T1 khung-không-đổi (tập Step.Order + Level.Order bất biến, :552-559 + :572-574 + :593-595 + :636-648) + T2 luật-chơi-không-đổi (Code/ApplicableType/CeoApprovalThreshold/F5/F2 = ECHO-only, lệch ⇒ 409, :539-550 + :598-603). Guard chữ-ký 2 tầng (:665-724): `CountLiveOpinionsAsync` + `ListRetainedOpinionsAsync` — cần kiểm 2 helper này có đọc `ContractSigningPlanLevelOpinions` không (xem §2/§4). Transaction rào @ :744-745.

**Deactivate-per-type khi POST:** `:339-343` — deactivate MỌI workflow `ApplicableType == typeEnum && IsActive` ⇒ **only-ONE-active-per-TYPE. Với 8 nhóm KHKK cùng type=10: tạo version mới cho N2 sẽ TẮT IsActive của N1..N8 còn lại** ⇒ đây là site chặn #1 (xem §2).

### §1-bis Phần sau của AdminFeatures (line 800-1205)

- **Guard chữ-ký 3-site ĐÃ phủ KHKK 3/3:** `CountLiveOpinionsAsync` :961-965 + `ListRetainedOpinionsAsync` :1033-1038 + `PurgeOrphanOpinionsAsync` :1099-1102 đều có nhánh `ContractSigningPlanLevelOpinions` (site liệt-kê-thủ-công, comment :957-960 tự khai luật "module mới PHẢI vào CẢ 3"). `DetachTrackedOpinionsOn` :1122-1136 quét theo TÊN CỘT metadata ⇒ tự phủ (comment :1120 nói "7 bảng" — stale nhẹ, thực tế 8).
- **🔴 GAP changelog PUT sửa-tại-chỗ:** `:863-893` chỉ ghi vết vào `PurchaseEvaluationChangelogs` khi `ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}` — **KHKK type-10 bị SKIP** (comment :857-862 tự khai: `ContractSigningPlanChangelogs` là changelog PHIẾU, "wire nếu cần ở W2/W3" — CHƯA wire). Sửa workflow nhóm KHKK đang có phiếu treo ⇒ phiếu 0 vết, chỉ có `def.UpdatedAt` (:851).
- **🔴 GAP DELETE version 0 usage-check:** `DeleteAwDefinitionCommandHandler` :1193-1205 = load + `Remove` + Save, **KHÔNG kiểm phiếu đang pin** (comment :1187-1189 tự khai "Sau UAT ... cần check usage trước khi delete" — nợ từ Mig 22 era). Hard-delete; hành vi thực tế phụ thuộc FK của cột pin từng module (cần đo FK `ContractSigningPlans.ApprovalWorkflowId` — xem §2).
- **SetUserSelectable** :1173-1185 generic by-Id ⇒ type-10 dùng được 0-đổi.

## §2 Seed 8 workflow nhóm KHKK + phân loại

**Seed = `DbInitializer.SeedKhkkGroupWorkflowsAsync`** (`Infrastructure/Persistence/DbInitializer.cs:549-687`, K3 nhịp-1 S164/S165) — KHÔNG phải Mig 71 (Mig 71 chỉ AddColumn `ApprovalGroup` lên phiếu + backfill=1; workflow ROWS do DbInitializer đẻ lúc boot, INFRASTRUCTURE seed not-gated DemoSeed, comment :539).

- **Phân loại 8 nhóm = `Code` string `KHKK-N{n}`** (:609), n=1..8 (:607). TẤT CẢ 8 cùng `ApplicableType=ContractSigningPlan(10)` (:616). Name `"Duyệt KHKK — Nhóm {n}"` (:617). Version=1, `IsActive=true` + `IsUserSelectable=true` cả 8 (:615,:622-623).
- **Idempotent PER-CODE** `AnyAsync(w.Code == code)` (:610) — comment :540-542 tự khai vì sao KHÔNG dùng khuôn per-type của 6 seeder mẫu: 8 workflow dùng chung 1 type, check type sẽ chặn 7 nhóm còn lại.
- **Đội hình 3 trạm PRO→CCM→BGĐ** sao từ QT-DN-V2-001 prod (roster :554-585): PRO C1 4-người OR-of-N + C2 tra.bui · CCM C1 4-người + C2 chuong.phan **`AllowApproverFinalize=true`** (:576-579) · BGĐ C1 truong.nguyen. Resolve theo EMAIL, thiếu user ⇒ bỏ slot + warn không throw (:593-600), cấp rỗng ⇒ nén Order (:645), workflow 0-bước vẫn tạo giữ mã (:673-676).
- **8 nhóm hiện SỐNG CHUNG IsActive=true cùng type=10** ⇒ bất biến "only-ONE-active-per-type" của POST create (`ApprovalWorkflowV2AdminFeatures.cs:339-343`) ĐÃ VỠ từ lúc seed (8 active cùng type). Hệ quả: **POST tạo version mới cho BẤT KỲ nhóm nào sẽ deactivate CẢ 8** — site chặn đích danh #1.
- Sidebar 8 menu-row `AwV2_KhkkN1..N8` seed @ `DbInitializer.cs:1988-1995`, parent `ApprovalWorkflowsV2`; MenuKeys VÀO `All` (`MenuKeys.cs:246-249`) ⇒ 32 policy `AwV2_KhkkN{n}.{R,C,U,D}` tồn tại.

## §3 Runtime duyệt KHKK — bảng 9-flag × {PE-runtime, KHKK-runtime}

KHKK runtime = `Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (W3 S161 + K3 S165). 4 action cố định `submit/approve/return/reject` (:74-75), KHÔNG có `targetPhase` param. OR-of-N `ResolveActingLevel` (:455-469), UPSERT `ContractSigningPlanLevelOpinions` (:477-513), con-trỏ ĐÔI reset-Cấp-1-khi-sang-Bước (:309-310), submit reset về Bước1/Cấp1 cả 2 lối vào (:198-202).

| # | Flag | PE-runtime | KHKK-runtime | Evidence KHKK |
|---|---|---|---|---|
| 1 | `AllowReturnOneLevel` | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:421` (switch `WorkflowReturnMode`) | **BỎ QUA** — return luôn = `TraLai` toàn phần | `ContractSigningPlanWorkflowService.cs:48-50` comment (c) "4 return-mode per-level CỐ Ý chưa đọc"; `ReturnOrRejectAsync:354-400` 0 đọc flag |
| 2 | `AllowReturnOneStep` | TÔN TRỌNG — `:422` | **BỎ QUA** — nt | nt |
| 3 | `AllowReturnToAssignee` | TÔN TRỌNG — `:423` | **BỎ QUA** — nt | nt |
| 4 | `AllowReturnToDrafter` | TÔN TRỌNG — `:424` (mode Drafter phải bật cờ) | **BỎ QUA (hành vi = luôn-cho)** — return về Drafter cho MỌI NV đúng lượt, kể cả cờ tắt | `ReturnOrRejectAsync` chỉ guard đúng-lượt (:379-380), không đọc cờ |
| 5 | `AllowApproverEditDetails` (F3) | TÔN TRỌNG — `PurchaseEvaluationDetailFeatures.cs:84` + `PurchaseEvaluationSupplierFeatures.cs:45` | **BỎ QUA / CHƯA WIRE** — KHKK Lines/Dossier edit = drafter-only Nháp/TraLai | `EnsureDraftEditable` `ContractSigningPlanFeatures.cs:295-303` (mọi đường ghi W2); 0 hit `AllowApproverEditDetails` trong ContractSigningPlan* |
| 6 | `AllowApproverEditBudget` (F4) | TÔN TRỌNG — `PurchaseEvaluationFeatures.cs:411` | **BỎ QUA / PE-semantics** (KHKK không có section NS; giá dòng = `ApprovedAmount ??= ProposedAmount` chốt tại finalize `:345-349`) | 0 hit trong ContractSigningPlan* |
| 7 | `AllowApproverSkipToFinal` (F2) | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:810` (+ Contract `:335`) | **CHƯA WIRE + BỊ ÉP-FALSE tại POST create cho type-10** | service 0 đọc (comment `:47` "giữ nguyên không port"); ép-false `ApprovalWorkflowV2AdminFeatures.cs:387` |
| 8 | `AllowApproverFinalize` (F5) | TÔN TRỌNG — `:870` (opt-out `applyLevelFinalize`) | **TÔN TRỌNG** — port @K3 S165, opt-out param mirror PE S96, set `EndedByLevelFinalize` runtime-flag (Mig 71) | `ContractSigningPlanWorkflowService.cs:276-289`; seed bật cho TP.CCM (`DbInitializer.cs:576-579,:655`) |
| 9 | `AllowApproverDelete` (F6) | TÔN TRỌNG — `PeSoftDeleteFeatures.cs:110-114` (3 rào: policy ∧ đúng-lượt ∧ F6) | **BỎ QUA / CHƯA WIRE** — delete KHKK không có nhánh approver-đúng-lượt (xem §3-bis delete handler) | 0 hit `AllowApproverDelete` trong ContractSigningPlan* |

- `CeoApprovalThreshold` (workflow-level, NGOÀI 9 checkbox): PE đọc; KHKK **CỐ Ý KHÔNG port** (NĐUQ human-gate vĩnh viễn, OG-3 — comment `:46-47`).
- DTO detail KHKK ĐÃ expose `AllowApproverFinalize` per-level (`ContractSigningPlanFeatures.cs:140,:1013,:1030`) — các flag khác không expose (PE expose đủ 9 @ `PurchaseEvaluationDtos.cs:124-138`).

### §3-bis Surprises quanh pin/đổi workflow (evidence create/update path)
- **Rào (v) group-match CHỈ chạy lúc CREATE:** `ContractSigningPlanFeatures.cs:479-486` regex `^KHKK-N([1-8])$` so `approvalGroup`; workflow code NGOÀI họ (type-10 code khác) → CHO QUA (comment :477-478).
- **🔴 UpdateDraft re-pin (:603-607) chỉ gọi `EnsureWorkflowTypeAsync` (type=10, :307-318), KHÔNG re-check group-match** ⇒ phiếu nhóm 3 tạo đúng KHKK-N3 rồi PUT draft re-pin KHKK-N5 = 200 OK; SubmitAsync cũng không re-check workflow-code↔group (`ContractSigningPlanWorkflowService.cs:169-187` chỉ check Lines↔group) ⇒ **bypass rào (v) 2-bước** — phiếu nhóm 3 chạy đội hình nhóm 5.
- FK `ContractSigningPlans.ApprovalWorkflowId` → ApprovalWorkflows = **Restrict vật lý** (`ContractSigningPlanConfiguration.cs:48-53`).

## §4 Gap-analysis BE → wave đề xuất

### §4.0 Authz đối chiếu 32 policy `AwV2_KhkkN{n}.{R,C,U,D}` (gotcha #85)
**Đo:** grep `AwV2_` toàn `SolutionErp.Api` = **0-hit** (control-dương cùng thước: `KeHoachKyKet` cùng path = 29 hit / 1 file ⇒ vắng thật). ⇒ **32 policy KHÔNG controller nào tiêu thụ** — chúng chỉ sống ở tầng menu-display (sidebar row + Permission matrix). Đây KHÔNG phải bug mới của KHKK: 2 key cũ `AwV2_DuyetNcc`/`AwV2_DuyetNccPhuongAn` (MenuKeys.cs:116-117) cũng 0 consumer BE — trang DuyetNcc hiện hành chạy đúng mô hình này rồi.
- **Key gate THẬT của trang admin (mọi type):** GET = authenticated-only · POST/PUT/PATCH/DELETE = `Workflows.Create` (§1). "Y như trang DuyetNcc" ⇒ **0-đổi-authz**: admin nào có `Workflows.Create` sửa được CẢ 8 nhóm (và cả PE/Contract workflows).
- **Trade-off nếu muốn per-group write-authz** (nhóm N1 chỉ ai được cấp N1 sửa): controller phải resolve workflow→Code→group rồi check `AwV2_KhkkN{n}.Update` — đổi hình authz của controller DÙNG CHUNG 9 type ⇒ blast radius mọi module; và theo #85 endpoint phải gate bằng key CỦA CHÍNH NÓ, không OR key con. **Quyết định thuộc owner/lead — không tự quyết**; mặc định rẻ + nhất quán = giữ `Workflows.Create`.
- 🔸 Menu-row 8 leaf cần `CanRead` grant per-role (KHKK root CỐ Ý không inherit — `GetMyMenuTreeQuery.cs` 4-case; seeder grant explicit) — tầng display độc lập tầng API (memory `permission_grant_two_layers`).

### §4.1 Disposition 4+1 site chặn
| # | Site | Hiện trạng | Đề xuất wave | Rủi ro |
|---|---|---|---|---|
| (a) | **only-ONE-active-per-TYPE** `ApprovalWorkflowV2AdminFeatures.cs:339-343` | POST version mới nhóm N2 ⇒ deactivate CẢ 8 (bất biến đã vỡ từ seed: 8 active cùng type-10). Blast thực tế: pick-list KhkkCreatePage lọc `isUserSelectable` KHÔNG lọc IsActive (`fe-admin/src/pages/khkk/KhkkCreatePage.tsx:97`) + runtime load by-Id ⇒ thiệt hại = Overview `Active` badge/summary + mọi chỗ đọc IsActive về sau | **P1 (khuyên):** đổi scope deactivate = `w.Code == request.Code` khi `typeEnum == ContractSigningPlan` (đúng semantic "1 active per chuỗi-version", 8 chuỗi độc lập). **P2:** đổi per-Code cho MỌI type — sạch hơn nhưng đổi hành vi PE/Contract (hiện tạo Code MỚI cùng type tự tắt Code cũ — có thể có chỗ dựa vào) | P1: if-đặc-thù type-10 (thêm 1 nhánh rẽ); P2: regression PE/Contract khó lường — cần grep chỗ đọc IsActive trước |
| (b) | **Changelog PUT skip type-10** `:863-893` | Sửa workflow nhóm có phiếu KHKK treo ⇒ phiếu 0 vết (comment :857-862 tự khai defer W2/W3) | Wire block mirror PE: query `ContractSigningPlans` pin `def.Id` + Phase ∉ {DaDuyet,TuChoi} → add `ContractSigningPlanChangelogs` (EntityType Workflow, khuôn `ContractSigningPlanWorkflowService.cs:546-557`) | Thấp — bảng phiếu-changelog có sẵn FK Cascade; chỉ thêm 1 nhánh else-if |
| (c) | **DELETE version 0 usage-check** `:1193-1205` | FK `ContractSigningPlans.ApprovalWorkflowId` **Restrict** (`ContractSigningPlanConfiguration.cs:50-53`) ⇒ data KHÔNG mất, nhưng `DbUpdateException` KHÔNG được map (`GlobalExceptionMiddleware.cs:32-39` chỉ map 5 app-exception, `_` → **500 "Lỗi hệ thống"**). Phiếu xoá-MỀM vẫn còn row ⇒ vẫn chặn. Workflow từng dùng rồi unpin: opinions FK Restrict vào Levels cũng chặn cascade ⇒ 500 | Guard usage-check trong `DeleteAwDefinitionCommandHandler`: đếm pin per-module (PE `ApprovalWorkflowId`, Contract, Proposal, 4 WorkflowApps, KHKK — 8/9 module có cột pin) + đếm LevelOpinions (tái dùng khuôn `CountLiveOpinionsAsync` NHƯNG theo workflow-id) → 409 đích danh "N phiếu đang pin" | Trung bình — liệt-kê-thủ-công 8 module (đúng class lỗi comment :957-960 đã cảnh báo); nợ này của Mig 22 era, sửa 1 lần lợi mọi type |
| (d) | **Bypass rào (v) 2-bước** `ContractSigningPlanFeatures.cs:603-607` | UpdateDraft re-pin chỉ check type, không check group-match; Submit không re-check ⇒ phiếu N3 chạy đội hình N5 | Extract rào (v) (:479-486) thành `ContractSigningPlanScope.EnsureWorkflowGroupMatchAsync(db, awId, group)` → gọi ở CẢ Create + UpdateDraft; cân nhắc gọi thêm ở `SubmitAsync` làm chốt-cuối (khuôn 2-tầng như hạng-mục :179-187) | Thấp; chú ý phiếu cũ đã lỡ pin lệch trước vá sẽ kẹt submit — message phải chỉ việc cần làm |
| (e) | **Ép-false F2 type-10** `:387` | ĐÚNG theo chống config-lie #78 (service KHKK 0 đọc F2 — `ContractSigningPlanWorkflowService.cs:47`) | **GIỮ** cho tới khi có lệnh owner port F2. Nếu port: copy nhánh PE `PurchaseEvaluationWorkflowService.cs:797-810` vào service KHKK + gỡ ép-false + gỡ khoá FE **CÙNG WAVE** (đúng tiền lệ F5 @K3: port + gỡ ép-false + gỡ FE-lock 1 wave, comment :376-386) | Mở mà không port = đẻ lại #78; port mà quên gỡ ép-false = cờ chết chiều ngược |

### §4.2 Việc 0-đổi-BE (đã generic sẵn — wave FE nối thẳng)
1. GET Overview `?applicableType=10` trả đủ 8 nhóm × versions × 9-flag per-level (§1) — FE lọc client-side theo `code == "KHKK-N{n}"` (payload nhỏ; muốn sạch hơn có thể thêm query param `code` — optional, không blocker).
2. PUT sửa-tại-chỗ (thêm/bớt người, đổi NV, 7 cờ tại-chỗ, đổi tên/phòng) — T1/T2 + guard chữ-ký 3-site ĐÃ phủ KHKK (§1-bis).
3. PATCH user-selectable + DELETE (khi chưa pin) — generic by-Id.
4. Lịch sử versions (History list per code) — có sẵn trong Overview DTO.
5. Runtime approve/return/reject/submit KHKK + F5 finalize + inbox + notify — W3/K3 xong (§3).
6. Authz: giữ `Workflows.Create` = y-hệt trang DuyetNcc (§4.0).

### §4.3 Việc PHẢI đổi BE (gom wave)
- **Wave-BE-1 (nhỏ, an toàn):** (a)-P1 deactivate-per-Code cho type-10 · (d) group-match guard 2-site · (b) changelog type-10 · (c) usage-check DELETE → 409. 4 việc đều trong 2 file `ApprovalWorkflowV2AdminFeatures.cs` + `ContractSigningPlanFeatures.cs` (+1 helper Scope), 0 migration.
- **Wave-BE-2 (chỉ khi owner duyệt semantics):** port return-modes F1×4 / F3-analog (sửa Lines lúc duyệt) / F6 approver-delete / F2 skip-to-final vào KHKK runtime — mỗi cái là ĐỔI HÀNH VI DUYỆT, thuộc quyền owner (comment (c) service :48-50 đã khai chủ-đích chưa đọc). Nếu KHÔNG port: FE phải ẩn/disable các ô chưa-wire cho type-10 (tiền lệ `earlyFinalizeLocked` W1 — đã có khuôn), không thì mỗi ô tick = config-lie #78 mới.

## §5 Surprises ngoài đề bài
1. **8 link sidebar KhkkN{n} hiện đổ về trang landing chung** — `Layout.tsx:188` route `/system/approval-workflows-v2/KhkkN{n}` nhưng `ApprovalWorkflowsV2Page.tsx` `TYPE_CODE_TO_INT` (:164-172) KHÔNG có key `KhkkN*` ⇒ `selectedTypeInt=undefined` → rơi về grid landing (:299-301,:323). Menu có, trang chưa phân nhóm — wave FE là phần nặng nhất của đề bài, BE gần xong.
2. **GET Overview 0-policy trả đủ 9-flag + CeoApprovalThreshold + tên/email toàn bộ approver cho MỌI user authenticated** (kể cả fe-user token) — cố ý từ Mig 22 cho Drafter pick (:10-15), nhưng payload nay giàu hơn nhiều so 2026-05; đáng để owner ngó lại (information-disclosure mềm).
3. **Roster 8 workflow = SNAPSHOT lúc seed theo email hardcode** (`DbInitializer.cs:554-585`) — QT-DN-V2-001 prod đổi người thì 8 nhóm KHÔNG tự theo; idempotent per-Code nên restart không re-sync (đúng thiết kế, nhưng dễ bị hiểu nhầm "sao đội hình không khớp DuyetNcc nữa").
4. **Bất biến only-1-active-per-type đã vỡ từ seed** (8 active type-10) mà Overview `Active = versions.FirstOrDefault(IsActive)` (:210) chỉ hiện 1 — 7 nhóm còn lại "tàng hình" khỏi ô Active của type-summary; FE per-group phải đọc theo Code, đừng tin ô `Active`.
5. `DetachTrackedOpinionsOn` comment "7 bảng" stale (thực 8 — quét metadata nên vẫn đúng hành vi) `ApprovalWorkflowV2AdminFeatures.cs:1120`.
6. KHKK delete phiếu allow-list ĐÃ rộng hơn PE: {Nháp, TuChoi, **TraLai**} (S177 owner) vs PE {DangSoanThao, TuChoi} + F6-đúng-lượt (`ContractSigningPlanFeatures.cs:1733-1738` vs `PurchaseEvaluationFeatures.cs:1404-1406` + `PeSoftDeleteFeatures.cs:110-114`) — mirror "nút XÓA ở màn duyệt" cho KHKK là F6-wire (wave-BE-2), không phải nới allow-list thêm.

## VERDICT
BE cho 8 workflow "Duyệt KHKK — Nhóm 1..8" đã **generic ~85%**: controller + 5 action + PUT sửa-tại-chỗ + guard chữ-ký + ghim/xoá + seed 8 nhóm (Code `KHKK-N{n}`, cùng ApplicableType=10) + runtime F5 finalize đều SỐNG. **4 site BE phải sửa** (wave-BE-1, 2 file, 0-mig): deactivate-per-Code type-10 (`:339-343`) · group-match guard thêm 2-site (`:603-607`+Submit) · changelog type-10 (`:863-893`) · usage-check DELETE→409 (`:1193-1205`). **6/9 checkbox hiện chỉ PE tôn trọng** — tick cho type-10 = cờ nằm im (#78-class): hoặc port runtime (wave-BE-2, cần owner duyệt semantics), hoặc FE khoá ô theo tiền lệ `earlyFinalizeLocked`. Authz giữ `Workflows.Create` y trang DuyetNcc; 32 policy `AwV2_KhkkN*` 0 consumer BE (đo + control-dương). Phần nặng còn lại là FE (8 route đổ landing chung).
