# REVIEW-1 (Fable, adversarial ĐÚNG-SAI) — S182 khkk-awf-mirror-ncc > Vai: `/fable-real reviewer` #1/2 — lens SAI/SÓT/MÂU-THUẪN trên spec DRAFT + 3 invest. > Trạng thái: HOÀN TẤT (3 đợt đo trực tiếp trên code + findings + wave-final). ## VERDICT **REVIEW-1: PASS_WITH_FIXES — 2C/5M** (+5m) - Kiến trúc spec ĐÚNG HƯỚNG, đa số claim load-bearing HELD khi đo lại trên code (History-full-list, deactivate-site, PUT-guards, rào-v, delete-0-guard, middleware, seeder, 0-consumer AwV2_). Verdict không phải FAIL vì không có finding nào lật kiến trúc — nhưng 2C (F1 unbuildable W1b, F2 IgnoreQueryFilters) PHẢI vào spec final trước khi execute, 5M nên vào cùng đợt. - C: **F1** (W1b helper internal ⟂ Submit Infrastructure — không biên dịch được như viết) · **F2** (W1d thiếu `IgnoreQueryFilters` ⇒ claim "hết 500" sai ở ca phổ biến nhất). - M: **F3** (5 office-pin LOOSE 0-FK — DB-invest M3 sai, W1d là rào duy nhất) · **F4** (khoá công thức W1a + belt + test code-lạ) · **F5** (KHKK-create không check IsActive — răng nút Ghim mô tả sai sau wave) · **F6** (D3=C: ô #4 phải forced-true, không mờ-chưa-có) · **F7** (ô Ngưỡng CEO tách khỏi D3, disable vô điều kiện). - m: F8 (khai mâu-thuẫn 3 lens) · F9 (baseline 645⟂644 — đo tươi) · F10 (môi trường acceptance #3) · F11 (audit pin-lệch tồn đọng) · F12 (hint lockedCode · notify-parity · comment `:339` · invalidate-keys OK). ## Checklist việc (8 mục từ lệnh) - [x] 1. W1a deactivate-per-Code — đọc `:326-398` + grep IsActive consumer toàn BE/Office → F4, kết luận P1 đủ (P2 không cần — 0 consumer server-side dựa active-per-type-10) - [x] 2. W2b synthetic slice — BE `:199-216` + FE `:286-353` → HELD (History = full list; slice đúng shape; queryKey/invalidate OK) - [x] 3. W2c lockedCode — PUT Code-echo 409 `:539-541` HELD; đường rơi-khỏi-slice còn lại = POST panel khác (chấp nhận, per-Code fix F4 khiến vô hại) → F12a hint - [x] 4. W1b group-match Submit — Submit `:163-213` không re-check; extract-helper UNBUILDABLE → F1; phiếu pin lệch tồn đọng → F11 - [x] 5. W1d DELETE — đếm từ Domain+snapshot: 8 cột pin, chỉ 3 FK, 5 LOOSE; ItTicket không pin → F2, F3 - [x] 6. D3 chấm A/B/C — A: giữ #78 6-ô + đẻ "sleeping-config" khi port sau; B: nặng, thuộc owner (service comment `:48-50` tự khai); C: khả thi có tiền lệ NHƯNG phải theo danh-sách F6 (#4 forced-true) + F7 tách CEO-box. Không chấm thay anh — cả 3 sống, C rẻ+trung thực nhất, A đúng "ko lệch" nhất (khai trade-off) - [x] 7. Mâu thuẫn 3 lens → F8 (a/b/c) - [x] 8. Sót chung → F12 (notify parity-PE không làm · invalidate keys ĐO OK · optimistic không dùng — trang này invalidate-only) ## GHI NHẬN SAU KHI ĐỌC 4 PIN (trước khi mở code) - Spec DRAFT 68 dòng, 3 invest đọc đủ. Nghi vấn dựng sẵn để đo: - (i) W2b: BE `History` trong `AwTypeSummaryDto` chứa TẤT CẢ version hay LOẠI active? Nếu loại → `versions.find(v=>v.isActive)` chết → active=null giả. PHẢI đọc `:199-213`. - (ii) W1a P1: scope `w.Code == request.Code` CÓ kèm `ApplicableType == typeEnum` không? UNIQUE(Code,Version) là GLOBAL — Code trùng xuyên-type khả thi về schema. - (iii) FE-invest D1 đề xuất scope theo REGEX `^KHKK-N[1-8]$`, BE-invest P1 theo typeEnum==10 — 2 công thức KHÁC NHAU (code `QT-KHKK-V2-001` rơi vào nhánh nào?). Spec chọn P1 mà không khai lệch lens. - (iv) W1b: rào (v) "code NGOÀI họ → CHO QUA" — extract giữ nguyên lỗ này? Phiếu ChoDuyet đã pin lệch TRƯỚC vá không bị đụng — cần SQL audit prod, spec chưa có. - (v) W1d: module pin loose-Guid (không FK) → hard-delete THÀNH CÔNG vật lý → dangling pin. Danh sách FK-Restrict của DB-invest chỉ có 3 (PE/Contract/KHKK) — WorkflowApps pins = FK hay loose? PHẢI đếm ModelSnapshot. - (vi) D3=C: ô #4 Trả-về-Drafter hành-vi = LUÔN-CHO dù cờ tắt → mờ kiểu "chưa áp dụng" là NÓI DỐI CHIỀU NGƯỢC; phải hiển thị forced-true. - (vii) Acceptance #3 (tạo v02 N1) chạy Ở ĐÂU? Prod = mutate config thật, local = workflow rỗng người. Không có activate-endpoint để rollback IsActive. ## ĐO TRỰC TIẾP (đợt 1 — AdminFeatures:80-420 + FE page:150-364) - **HELD (i):** `AwTypeSummaryDto.History` = FULL list mọi version (BE `:206-211` — `versions` dùng cho CẢ `Active` lẫn `History`) ⇒ synthetic slice W2b `history.filter(code)` + `.find(isActive)` ĐÚNG shape. Ordering: query `OrderByDescending(Version)` ⇒ slice giữ Version DESC per code — đủ cho render. - **HELD:** deactivate `:340-343` đúng như invest tả: `Where(ApplicableType == typeEnum && IsActive)`. Comment dòng 339 tự khai "only ONE active per type". - **Đo thêm:** `nextVersion` `:334-337` đã tính theo **Code** (per-Code sẵn) — chỉ deactivate là per-type ⇒ vá P1 là nhất quán hoá scope với chính hàm. - **Đo FE:** queryKey `['approval-workflow-v2-overview', selectedTypeInt]` (`:292`) — 8 kind nhóm sẽ share key với panel ContractSigningPlan (cùng 10) = cache chung, invalidate `:348` prefix-match toàn key ⇒ mutation từ panel nhóm tự làm tươi panel type-10 và ngược lại. KHÔNG cần key mới. - **Đo FE:** `DEFAULT_CODE_BY_TYPE[10]='QT-KHKK-V2-001'` (`:179`) — nút "Tạo quy trình mới" từ panel ContractSigningPlan (không lockedCode) vẫn đề xuất code NGOÀI-họ type-10. Sau P1 (scope theo typeEnum==10 ⇒ per-Code) code lạ này KHÔNG tắt 8 nhóm. Nếu executor lỡ implement theo công-thức FE-invest D1 (regex `^KHKK-N[1-8]$`) thì code lạ rơi nhánh per-type ⇒ NUKE cả 8 nhóm — 2 công thức KHÁC HÀNH VI, spec phải khoá công thức. ## ĐO TRỰC TIẾP (đợt 2 — guards/delete/FK/middleware/scope/fe-user) - **HELD:** PUT T2 Code-echo 409 (`:539-541`) · CEO-echo (`:547-550`) · F5/F2-echo (`:598-603`) · ô Mã FE `disabled={isEdit}` (`:927`) · CEO box `disabled={isEdit}` only (`:954` — CREATE-mode mở cho MỌI type kể cả 10). - **HELD:** DELETE handler `:1196-1204` = load+Remove+Save, 0 guard; comment `:1187-1189` tự khai nợ. Middleware chỉ map 5 app-exception (`GlobalExceptionMiddleware.cs:32-40`), `_` → 500. - 🔴 **MỚI — pin FK thật:** grep `HasForeignKey("ApprovalWorkflowId")` snapshot = **4 hit** (Steps-Cascade + KHKK/Contract/PE-Restrict). Domain có **8 cột pin** (thêm Proposal/Leave/Ot/Vehicle/Travel — ItTicket KHÔNG có) ⇒ **5 module Office pin = LOOSE 0-FK** ⇒ DELETE hôm nay với office-pin THÀNH CÔNG vật lý → dangling pin. DB-invest M3 "data KHÔNG mất (DB chặn được)" = SAI cho 5 module này. W1d usage-check là guard DUY NHẤT cho office. - 🔴 **MỚI — query filter:** PE (`:84`) + Contract (`:44`) + KHKK (`:55`) đều `HasQueryFilter(!IsDeleted)` ⇒ đếm-pin naive sẽ BỎ SÓT phiếu xoá-mềm (FK vẫn chặn 547) ⇒ W1d PHẢI `IgnoreQueryFilters()` cho 3 module này, không thì đúng ca "đã xoá phiếu rồi mà vẫn 500" tồn tại tiếp. - **HELD:** 8 bảng `*LevelOpinion` (8 FK `ApprovalWorkflowLevelId`) khớp claim "CẢ 8". - 🔴 **MỚI — W1b unbuildable như viết:** `ContractSigningPlanScope` = `internal static` (`ContractSigningPlanFeatures.cs:263`); `InternalsVisibleTo` CHỈ có `SolutionErp.Infrastructure.Tests` (csproj:25). Submit KHÔNG có Application-handler (comment `:19` — controller gọi thẳng service Infrastructure) ⇒ helper internal KHÔNG gọi được từ `SubmitAsync`. Executor phải chọn: public-guard-class mới (Application) / inline-duplicate / InternalsVisibleTo-Infrastructure. Spec chưa khai. - **HELD:** `ContractSigningPlanEntityType.Workflow = 4` tồn tại + changelog entity đủ cột ⇒ W1c 0-enum-extend. Phase filter {DaDuyet=3, TuChoi=99} đúng tên enum. - **HELD:** rào (v) `:479-486` cho-qua code ngoài-họ LÀ CHỦ ĐÍCH (comment :477-478). UpdateDraft `:603-607` chỉ EnsureWorkflowTypeAsync. Submit `:163-213` chỉ check Lines↔group. - 🔴 **MỚI — IsActive server-side cho KHKK-create = KHÔNG CÓ:** bridge KHKK→HĐ đã vá đòi `IsActive ∧ IsUserSelectable` (vá-3, `CreateContractFromSigningPlanFeatures.cs:141-147` — chính comment nêu "khuôn ContractFeatures thiếu 2 vế ⇒ forge POST pin được quy trình retired"); Create/UpdateDraft phiếu KHKK thì CHỈ check type ⇒ pin bản archived = 200. fe-user KhkkCreatePage `:104` filter `isUserSelectable` KHÔNG lọc `isActive`, `:114` `find(code)` ăn theo ordering Version-DESC. Hệ quả SAU W1a: (a) tạo v02 xong, v01 vẫn selectable=true (POST không đụng cờ này) — auto-pin trúng v02 chỉ NHỜ ordering; (b) "Bỏ ghim v02" ⇒ rơi về v01 archived im lặng, KHÔNG chặn tạo như spec mô tả răng nút Ghim. - **Grep IsActive consumer (W1a regression):** Office features 0 lọc IsActive trên ApprovalWorkflows (mọi hit IsActive = Users/MeetingRooms/OtPolicies/Catalog) · V1 `WorkflowDefinitions.IsActive` không liên quan · consumer THẬT của IsActive-V2: Overview `Active` (display) + bridge type-3 vá-3 + FE dropdowns (PE/Contract create lọc client-side). KHÔNG AI resolve "active-per-type-10" server-side ⇒ P1 đổi scope type-10 an toàn, P2 không cần. ## ĐO TRỰC TIẾP (đợt 3 — service/seeder/status/FE-designer) - **HELD:** service KHKK comment `:36-65` tự khai (c) 4 return-mode CỐ Ý chưa đọc + "mở thêm mode là đổi hành vi duyệt, thuộc quyền lead" · finalize `:276-289` = #8 sống thật · Submit là entry-point controller-gọi-thẳng (`:60-62` — xác nhận KHÔNG có MediatR handler cho Submit). - **HELD:** seeder `DbInitializer.cs:539-625` đúng như invest (idempotent per-Code `:610`, roster email prod, `IsActive=true + IsUserSelectable=true` `:622-623`, thiếu user → bỏ slot + warn). - **HELD (control):** grep `AwV2_` trong `SolutionErp.Api` = **0 hit** — khớp claim "32 policy 0-consumer BE". - **Đo FE Designer:** "Tạo quy trình mới" (`TypePanel:413`) set `cloneFrom = type.active` ⇒ ở panel nhóm (synthetic slice) code state tự prefill `KHKK-N{g}` từ card active — lockedCode là RÀO chống sửa tay + chống case active=null (rơi về `DEFAULT_CODE_BY_TYPE[10]='QT-KHKK-V2-001'` `:650`). Archived card cũng đủ 4 nút (`:426-443`) — slice pass-through by-Id OK. - **Đo STATUS.md:** canonical dòng 8 = **"644 test PASS (45D+599I — CI #444 KHỚP local)"** @S168; mốc sống S179 không ghi đổi test. Spec W4 ghi "baseline 645" — lệch 1, không rõ nguồn. ## FINDINGS > Ký hiệu: `[C]` phải sửa spec trước khi execute · `[M]` lỗ/claim-sai phải vá trong spec · `[m]` khai/polish. Mỗi finding: chỗ sai (spec/invest) → bằng chứng file:line → cách vá. **F1 [C] — W1b không biên dịch được như viết (helper internal ⟂ Submit nằm Infrastructure).** Spec W1b: *"extract rào (v) → `EnsureWorkflowGroupMatchAsync(db, awId, group)`; gọi ở Create + UpdateDraft + Submit"*. Đo: `ContractSigningPlanScope` = `internal static` (`ContractSigningPlanFeatures.cs:263`); `InternalsVisibleTo` duy nhất = `SolutionErp.Infrastructure.Tests` (`SolutionErp.Application.csproj:25`); Submit KHÔNG có Application-handler — controller gọi thẳng service Infrastructure (`ContractSigningPlanFeatures.cs:19` + `ContractSigningPlanWorkflowService.cs:60-62`) ⇒ helper để trong Scope thì site thứ 3 KHÔNG gọi được, executor sẽ tự chế đường khác ngoài review. **Vá:** tạo `public static class ContractSigningPlanWorkflowGuards` (Application/ContractSigningPlans, file cạnh Scope) chứa helper; Create/UpdateDraft (Application) + SubmitAsync (Infrastructure — đã reference Application) cùng gọi. KHÔNG dùng `InternalsVisibleTo(Infrastructure)` (mở toàn bộ internal surface 2 assembly) · KHÔNG inline-duplicate (drift 3 bản regex). **F2 [C] — W1d claim "hết cảnh 547 → 500" là claim-mạnh-hơn-fix: thiếu `IgnoreQueryFilters()`.** PE + Contract + KHKK đều có `HasQueryFilter(!IsDeleted)` (`PurchaseEvaluationConfiguration.cs:84` · `ContractConfiguration.cs:44` · `ContractSigningPlanConfiguration.cs:55`). Đếm-pin naive sẽ KHÔNG THẤY phiếu xoá-mềm, nhưng FK vật lý vẫn chặn (row còn) ⇒ đúng ca PHỔ BIẾN NHẤT (drafter xoá phiếu nháp/TraLai — allow-list KHKK S177 — rồi admin xoá version "không ai dùng") vẫn 547→500. Test trong spec ("DELETE version có pin ⇒ 409") PASS mà bug ship — acceptance đo hình-dạng không đo hành-vi (@S179 class). **Vá:** mọi count pin/opinion trong guard dùng `IgnoreQueryFilters()` (3 module có filter; 5 office không filter — đếm thẳng); opinions đếm CẢ row phiếu-đã-xoá (FK không quan tâm IsDeleted). Test bổ sung: phiếu KHKK đã xoá-mềm pin version → DELETE 409. **F3 [M] — DB-invest M3 "data KHÔNG mất (DB chặn được)" SAI cho 5 module Office — pin LOOSE 0-FK; W1d là guard DUY NHẤT.** Đo snapshot: `HasForeignKey("ApprovalWorkflowId")` = **4 hit** (Steps-Cascade `:6231` + KHKK `:6247` + Contract `:6329` + PE `:6757`). Domain có **8 cột pin** (`Proposal/LeaveRequest/OtRequest/VehicleBooking/TravelRequest` thêm vào 3 module trên; ItTicket KHÔNG có — "8/9" của BE-invest ĐÚNG). ⇒ DELETE hôm nay với phiếu Office pin (0 opinions) **thành công vật lý** → dangling pin → phiếu treo chết lúc load workflow. Spec W1d đếm đủ 8 module (đúng), nhưng rationale kế thừa claim sai của DB-invest — executor đọc M3 có thể thu hẹp còn 3 module FK "vì DB chặn rồi". **Vá spec:** sửa rationale W1d thành 2 vế: (a) 3 module FK = nâng UX 547→409; (b) 5 module Office = **chống mất-toàn-vẹn dữ liệu** (không có FK đỡ). Test BẮT BUỘC ca office: Leave/Proposal phiếu pin → DELETE 409 (hôm nay = 200-xoá-êm). **F4 [M] — W1a: 2 công thức scope KHÁC HÀNH VI giữa 2 invest, spec phải KHOÁ công thức + thiếu 1 test + thiếu belt.** BE-invest P1 = `typeEnum == ContractSigningPlan ⇒ scope Code`; FE-invest D1 = *"per-Code khi code khớp `^KHKK-N[1-8]$`"*. Khác nhau ở code type-10 NGOÀI-họ (`QT-KHKK-V2-001` — chính là `DEFAULT_CODE_BY_TYPE[10]` `:179` mà panel ContractSigningPlan đề xuất): công thức BE → per-Code (an toàn); công thức FE-regex → rơi nhánh per-type → **NUKE cả 8 nhóm**. Spec chọn đúng (typeEnum) nhưng khai "3 lens hội tụ" — che lệch; executor đọc FE-invest có thể implement regex. **Vá:** (a) KHOÁ công thức trong spec: `w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive` (vế `ApplicableType` = belt: UNIQUE(Code,Version) là GLOBAL nên 1 Code có thể span 2 type qua version khác nhau — hiếm nhưng rẻ để chặn); (b) thêm test thứ 3: POST type-10 code-lạ `QT-KHKK-V2-001` ⇒ IsActive 8 nhóm bất biến; (c) ghi chú P1 nhất quán với chính hàm — `nextVersion` `:334-337` đã per-Code sẵn. **F5 [M] — KHKK-create KHÔNG check `IsActive` server-side ⇒ sau wave này, nút Ghim mất răng như spec mô tả + pin-bản-archived = 200.** Đo: Create/UpdateDraft phiếu KHKK chỉ `EnsureWorkflowTypeAsync` (type) + rào (v) — KHÔNG đòi IsActive/IsUserSelectable; đối chứng bridge KHKK→HĐ ĐÃ vá đúng lỗ này cho type-3 (vá-3, `CreateContractFromSigningPlanFeatures.cs:141-147`, comment tự khai "forge POST pin được quy trình retired"). FE `KhkkCreatePage` fe-user `:104` filter `isUserSelectable` KHÔNG lọc `isActive`, `:114-116` `find(code)` ăn may theo ordering Version-DESC của BE. Hệ quả CHỈ LỘ SAU W2 (mở đường tạo v02): (a) v01 sau deactivate vẫn `IsUserSelectable=true` (POST `:343` không đụng cờ này) ⇒ "Bỏ ghim v02" làm auto-pin **rơi về v01 archived im lặng** thay vì CHẶN tạo — câu regression của spec (*"Bỏ ghim ⇒ CHẶN TẠO PHIẾU nhóm đó"*) chỉ đúng khi nhóm có đúng 1 version; (b) forge POST pin bản archived. **Vá (khuyến nghị làm trong W1):** Create + UpdateDraft-khi-đổi-pin thêm check `IsActive ∧ IsUserSelectable` 2 message tách (mirror vá-3); UpdateDraft giữ-nguyên-pin không đụng (guard chỉ chạy `awId != plan.ApprovalWorkflowId` `:603` — phiếu cũ pin bản archived vẫn sửa GhiChu bình thường). FE ×2 app: `find(w.isActive && code-match)` (fe-user `:114` + fe-admin mirror `:113-116`). Nếu anh muốn giữ nguyên semantics cũ thì tối thiểu PHẢI sửa câu răng-nút-Ghim trong spec thành "chặn khi bỏ ghim TẤT CẢ version của nhóm". **F6 [M] — D3=C như spec viết sẽ NÓI DỐI CHIỀU NGƯỢC ở ô #4 Trả-về-Drafter; danh sách ô mờ phải CHÍNH XÁC.** Bảng BE §3 dòng #4: KHKK **BỎ QUA cờ nhưng hành-vi = LUÔN-CHO** (return về Drafter cho mọi NV đúng lượt kể cả cờ tắt — `ReturnOrRejectAsync` không đọc cờ). Mờ ô #4 kiểu "chưa áp dụng cho KHKK" = bảo tính năng KHÔNG CÓ trong khi nó LUÔN BẬT. **Vá spec-C (danh sách chốt, gate `applicableType === 10` — áp cả panel ContractSigningPlan cho nhất quán):** - Mờ-**false**-disabled + tooltip "chưa áp dụng cho KHKK": #1 `allowReturnOneLevel` · #2 `allowReturnOneStep` · #3 `allowReturnToAssignee` (`:1235-1266` vùng render) · #5 `allowApproverEditDetails` `:1268-1276` · #6 `allowApproverEditBudget` `:1277-1285` · #9 `allowApproverDelete` `:1320-1328` — 6 ô. - **Forced-true**-disabled + tooltip "KHKK luôn trả về người soạn khi Trả lại — cờ không tắt được": #4 `allowReturnToDrafter` `:1256-1267`. - Giữ nguyên: #7 đã disabled sẵn (`:1292-1302`) · #8 sống (`:1307-1315`). - Payload: ô mờ gửi giá-trị-đang-lưu (echo, không ép) để không vấp guard PUT. **F7 [M] — Ô "Ngưỡng giá trị gói CEO" phải TÁCH KHỎI D3 — disable cho type-10 VÔ ĐIỀU KIỆN.** `:944-961` chỉ `disabled={isEdit}` ⇒ create-mode NHẬP ĐƯỢC cho type-10, trong khi `CeoApprovalThreshold` là quyết-đã-chốt-vĩnh-viễn KHÔNG port (OG-3 NĐUQ human-gate — `ContractSigningPlanWorkflowService.cs:46-47`). Lý lẽ của phương án A ("cờ nằm chờ port") KHÔNG áp dụng cho ô này — nó là #78 vĩnh viễn, không phải tạm thời. Spec hiện bó nó vào C ⇒ anh chọn A hoặc B là lỗ này sống tiếp. **Vá:** W2 thêm mục riêng (mọi phương án D3): type-10 ⇒ ô CEO disabled + tooltip "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST gửi `null`; tiền lệ cùng-file `:1292`. **F8 [m] — Mâu-thuẫn 3 lens spec chưa khai (mục 7 của đề):** (a) công-thức W1a lệch BE⟂FE (đã gộp F4); (b) D3: FE-invest §3 *"mặc định nghiêng GIỮ (A)"* ⟂ BE-invest §4.3 *"KHÔNG port thì FE PHẢI ẩn/disable"* (= C) — spec trình trung lập mà không khai 2 lens ngược nhau; (c) DB-invest W-DB-2 (thêm chiều `code=` phía BE) bị DROP im lặng — spec chọn client-side slice mà không ghi disposition. **Vá:** 3 dòng khai trong MỤC 3. **F9 [m] — Baseline test hardcode "645" ⟂ STATUS canonical "644" (@S168 CI #444).** Một trong hai stale. **Vá:** W0/W4 đo tươi + ghi vào run.md, spec không chép số (B1). **F10 [m] — Acceptance #3 chưa chỉ định môi trường; prod-create v02 là mutate config THẬT và KHÔNG có đường re-activate** (controller 5 action — không có activate; PUT cấm đổi IsActive; seeder idempotent per-Code không re-set). **Vá:** chứng chính = integration test W1a; acceptance UI #3 chạy LOCAL sau W0 (chấp nhận workflow rỗng người — đo cấu-trúc slice/badge); nếu muốn prod thì v02 = clone-y-hệt và khai với anh "v02 thành bản sống, v01 archived" TRƯỚC khi bấm. **F11 [m] — Thiếu data-audit phiếu ĐÃ pin lệch trước vá.** Guard W1b chỉ chặn Create/UpdateDraft/Submit TỪ NAY; phiếu ChoDuyet pin lệch từ trước chạy tiếp đội hình lệch im lặng. **Vá W5:** thêm query `SELECT p.MaKeHoach, p.ApprovalGroup, w.Code FROM ContractSigningPlans p JOIN ApprovalWorkflows w ON w.Id = p.ApprovalWorkflowId WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup)` — >0 row thì báo anh xử tay. Cùng W5: nếu query 8-nhóm cho thấy nhóm nào 0-active (bom đã nổ trước vá) → recovery = "Tạo phiên bản mới" từ panel nhóm (lockedCode tự điền mã đúng). **F12 [m] — Sót nhỏ (mục 8):** (a) hint ô Mã `:929-932` khi `lockedCode` nên đổi chữ ("Mã khoá theo nhóm — version auto-tăng"); (b) sửa-workflow-tại-chỗ KHÔNG notify approver phiếu treo — parity PE (PE cũng không notify), KHÔNG làm wave này, khai vào Nợ; (c) invalidate keys ĐÃ ĐO ĐỦ: key `['approval-workflow-v2-overview', selectedTypeInt]` + mutations invalidate prefix ⇒ 8 panel nhóm và panel type-10 dùng chung cache tự tươi, 0 việc; fe-user cache là app khác — không cross-invalidate được, chấp nhận (hành vi sẵn có); (d) comment BE `:339` "only ONE active per type" phải sửa CÙNG COMMIT với W1a kẻo comment nói dối. ## TỔNG HỢP WAVE FINAL ĐỀ XUẤT > Bản wave-plan ĐÃ SỬA theo F1-F12 — đủ chi tiết cho executor Opus MAX. Chỗ nào giữ nguyên spec DRAFT thì ghi "như spec". ### W0 — prep local (như spec) + 2 bổ sung 1. `dotnet ef database update` ×2 DB (Dev + Design — đo lại drift tại chỗ, đừng tin số cũ) → boot API 1 lần → 8 row `KHKK-N*` local (RỖNG NGƯỜI là ĐÚNG). 2. **[F9]** `dotnet test SolutionErp.slnx` đo baseline TƯƠI, ghi số vào `run.md` (spec 645 ⟂ STATUS 644 — không chép, đo). 3. **[F10]** Chốt môi trường acceptance: #1/#2 local + prod; #3 = integration test (chứng chính) + UI local; prod-#3 CHỈ khi anh gật "v02 clone thành bản sống". ### W1 — BE: 5 fix, 2 file + 1 file MỚI, 0-mig | # | Fix | Site | Cách (đã sửa theo finding) | |---|---|---|---| | a | Deactivate-per-Code type-10 **[F4]** | `ApprovalWorkflowV2AdminFeatures.cs:339-343` | CÔNG THỨC KHOÁ: `typeEnum == ContractSigningPlan` ⇒ `Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)`; type khác giữ per-type. CẤM biến-thể regex `^KHKK-N[1-8]$` của FE-invest D1 (code lạ type-10 sẽ rơi per-type → nuke 8 nhóm). Sửa comment `:339` cùng commit **[F12d]** | | b | Group-match guard 3-site **[F1]** | Create `:479-486` · UpdateDraft `:603-607` · `SubmitAsync` (`ContractSigningPlanWorkflowService.cs`, sau guard pin `:163-165`) | File MỚI `Application/ContractSigningPlans/ContractSigningPlanWorkflowGuards.cs` — `public static class`, method `EnsureWorkflowGroupMatchAsync(IApplicationDbContext, Guid awId, int approvalGroup, CancellationToken)` (move nguyên logic rào (v), GIỮ semantics cho-qua-ngoài-họ + khai trong spec). 3 site cùng gọi. Message 409: "Phiếu nhóm N{g} đang gắn quy trình N{k} — vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình." | | b2 | **MỚI [F5]** IsActive∧IsUserSelectable cho pin KHKK | Create (sau rào iv) + UpdateDraft (trong nhánh `awId != plan.ApprovalWorkflowId` `:603`) | Mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147` — 2 message tách "đã ngừng hoạt động"/"chưa ghim". Giữ-nguyên-pin KHÔNG check (phiếu cũ pin bản archived vẫn sửa GhiChu được) | | c | Changelog type-10 khi PUT **(như spec, chi tiết chốt)** | `:863-893` | Nhánh `else if (def.ApplicableType is ContractSigningPlan)`: query `db.ContractSigningPlans.Where(p => p.ApprovalWorkflowId == def.Id && p.Phase != DaDuyet && p.Phase != TuChoi)` (query-filter tự loại phiếu xoá-mềm — ĐÚNG cho changelog) → add `ContractSigningPlanChangelogs { EntityType = Workflow /*=4, enum sẵn*/, Action = Update, EntityId = def.Id, PhaseAtChange, UserId/UserName, Summary + ContextNote }` mirror block PE | | d | DELETE usage-check → 409 **[F2][F3]** | `:1193-1205` | Đếm pin **8 module** (PE/Contract/KHKK **BẮT BUỘC `IgnoreQueryFilters()`**; Proposal/Leave/Ot/Vehicle/Travel đếm thẳng — 5 module này pin LOOSE 0-FK, guard này là RÀO DUY NHẤT chống dangling) + đếm opinions 8 bảng theo level-ids của workflow (IgnoreQueryFilters — FK không quan tâm IsDeleted) → 409 kể tên "N phiếu đang gắn + M chữ ký". Rationale 2 vế: FK-modules = UX 547→409; office = data-integrity | **Test W1 (test-before a/b/b2, test-after c/d):** 1. Tạo v2 `KHKK-N1` ⇒ N2..N8 IsActive bất biến. 2. DuyetNcc tạo Code-mới-cùng-type vẫn tắt Code cũ (regression per-type). 3. **[F4]** POST type-10 code-lạ `QT-KHKK-V2-001` ⇒ 8 nhóm bất biến. 4. UpdateDraft re-pin lệch nhóm ⇒ 409; re-pin bản archived ⇒ 409 **[F5]**; Submit phiếu pin lệch ⇒ 409. 5. PUT workflow nhóm có phiếu treo ⇒ 1 row `ContractSigningPlanChangelogs`. 6. DELETE version có pin KHKK ⇒ 409; **phiếu KHKK ĐÃ XOÁ MỀM pin ⇒ vẫn 409 [F2]**; **phiếu Leave (office, 0-FK) pin ⇒ 409 [F3]**; version 0-usage ⇒ xoá OK. ### W2 — FE `ApprovalWorkflowsV2Page.tsx` (~70-100 dòng) + KhkkCreatePage ×2 | # | Chạm | Site | Cách | |---|---|---|---| | a | Parse kind (như spec) | `:164-172` | `match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? 10 : TYPE_CODE_TO_INT[typeCode]` | | b | Synthetic slice (như spec — ĐÃ VERIFY `History` = full list kể cả active, BE `:206-211`) | `:299-301` | `versions = type10.history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v => v.isActive) ?? null` · label = "Duyệt KHKK — Nhóm {g}" + phụ đề `khkkGroupMenuLabel(g)` (D4) | | c | `lockedCode` (như spec) + hint **[F12a]** | `TypePanel:357` → `Designer:620/:650-651/:798/:920-933` | prop `lockedCode?: string`; ô Mã `disabled={isEdit \|\| !!lockedCode}` + hint "Mã khoá theo nhóm"; POST gửi `lockedCode ?? code` | | d | D3 — thi hành theo anh chốt; **nếu C thì theo danh-sách-chốt [F6]** | `:1235-1328` | 6 ô mờ-FALSE (#1#2#3 return-modes · #5 `:1268-1276` · #6 `:1277-1285` · #9 `:1320-1328`) + **#4 FORCED-TRUE-disabled** tooltip "KHKK luôn trả về người soạn — cờ không tắt được" + #7 giữ disabled sẵn + #8 sống. Gate `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (áp cả panel ContractSigningPlan). Payload ô mờ = echo giá-trị-đang-lưu | | e | (optional, như spec) mirror menuKeys +8 const | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | 0 hệ quả runtime | | f | **MỚI [F7]** — Ô Ngưỡng CEO: disable type-10 VÔ ĐIỀU KIỆN (mọi phương án D3) | `:944-961` | `disabled={isEdit \|\| applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}` + tooltip "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST type-10 gửi `null`. Tiền lệ `:1292` | | g | **MỚI [F5]** auto-pin lọc isActive ×2 app | fe-user `KhkkCreatePage.tsx:114` + fe-admin `:113-116` | `find(w => w.isActive && codeMatch)` — giữ filter `isUserSelectable` `:104`. Răng nút Ghim đúng lại: bỏ ghim BẢN ACTIVE = chặn tạo phiếu nhóm | **Regression (như spec) + sửa câu răng-Ghim theo F5.** SHA-mirror: KHÔNG áp cho Designer (fe-admin-only, đã chứng); RIÊNG W2g là cặp file 2-app — diff phải mirror logic (không cần byte-equal vì 2 file vốn khác nhau). ### W3 — CHỈ KHI D3=B (như spec; anchors `PeSoftDeleteFeatures.cs:110-114` / `PurchaseEvaluationWorkflowService.cs:421-424/:810` CHƯA re-đo trong review này — executor verify trước khi port; mỗi cờ 1 test RED→GREEN; `CeoApprovalThreshold` vĩnh viễn KHÔNG port — F7 vẫn áp) ### W4 — test-specialist đóng suite: baseline = SỐ ĐO Ở W0 (không phải 645) + N test mới W1/W3 ### W5 — reviewer pre-commit → commit → cicd 3-chân (BE api + bundle admin rotate; +eoffice nếu W2e/W2g fe-user) → acceptance + SQL verify - 3 query DB §4 (như spec) + **[F11]** query audit pin-lệch: `... WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup)` — >0 báo anh xử tay. - Query 8-nhóm: nhóm nào 0-active (bom nổ trước vá) → recovery = "Tạo phiên bản mới" từ panel nhóm đó (lockedCode tự điền mã, sinh vNext active). - Acceptance 1-4 như spec, #3 theo môi trường đã chốt ở W0.3 **[F10]**. ### Điểm quyết cho anh (cập nhật theo review) - **D3** giữ 3 phương án NHƯNG: (i) khai 2 lens khuyến nghị ngược nhau (FE nghiêng A, BE nghiêng C) **[F8b]**; (ii) nếu C → dùng danh-sách-chốt F6 (ô #4 forced-true, KHÔNG mờ-kiểu-chưa-có); (iii) ô Ngưỡng CEO KHÔNG thuộc D3 — disable vô điều kiện **[F7]**. - **D4** (title panel) giữ như spec. - **MỚI D5 [F5]:** chốt hành vi pin: (khuyến nghị) thêm check IsActive∧IsUserSelectable + FE lọc isActive — hay giữ nguyên semantics cũ và sửa lại câu răng-nút-Ghim trong spec. ### Nợ NGOÀI scope (giữ như spec, +2) GET Overview 0-policy (information-disclosure mềm) · roster snapshot-email · `Khkk_WfView` panel aggregate giấu 7/8 · comment "7 bảng" `:1120` · **+ sửa-workflow-tại-chỗ không notify approver phiếu treo (parity PE) [F12b]** · **+ W-DB-2 (param `code=` phía BE) = KHÔNG LÀM, chọn client-side slice — disposition khai [F8c]**.