21 KiB
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ánhContractSigningPlanLevelOpinions(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-893chỉ ghi vết vàoPurchaseEvaluationChangelogskhiApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}— KHKK type-10 bị SKIP (comment :857-862 tự khai:ContractSigningPlanChangelogslà 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 FKContractSigningPlans.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 =
CodestringKHKK-N{n}(:609), n=1..8 (:607). TẤT CẢ 8 cùngApplicableType=ContractSigningPlan(10)(:616). Name"Duyệt KHKK — Nhóm {n}"(:617). Version=1,IsActive=true+IsUserSelectable=truecả 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..N8seed @DbInitializer.cs:1988-1995, parentApprovalWorkflowsV2; MenuKeys VÀOAll(MenuKeys.cs:246-249) ⇒ 32 policyAwV2_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
AllowApproverFinalizeper-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-486regex^KHKK-N([1-8])$soapprovalGroup; 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-187chỉ 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.Createsử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
CanReadgrant per-role (KHKK root CỐ Ý không inherit —GetMyMenuTreeQuery.cs4-case; seeder grant explicit) — tầng display độc lập tầng API (memorypermission_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)
- GET Overview
?applicableType=10trả đủ 8 nhóm × versions × 9-flag per-level (§1) — FE lọc client-side theocode == "KHKK-N{n}"(payload nhỏ; muốn sạch hơn có thể thêm query paramcode— optional, không blocker). - 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).
- PATCH user-selectable + DELETE (khi chưa pin) — generic by-Id.
- Lịch sử versions (History list per code) — có sẵn trong Overview DTO.
- Runtime approve/return/reject/submit KHKK + F5 finalize + inbox + notify — W3/K3 xong (§3).
- 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ệ
earlyFinalizeLockedW1 — đã có khuôn), không thì mỗi ô tick = config-lie #78 mới.
§5 Surprises ngoài đề bài
- 8 link sidebar KhkkN{n} hiện đổ về trang landing chung —
Layout.tsx:188route/system/approval-workflows-v2/KhkkN{n}nhưngApprovalWorkflowsV2Page.tsxTYPE_CODE_TO_INT(:164-172) KHÔNG có keyKhkkN*⇒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. - 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).
- 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"). - 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. DetachTrackedOpinionsOncomment "7 bảng" stale (thực 8 — quét metadata nên vẫn đúng hành vi)ApprovalWorkflowV2AdminFeatures.cs:1120.- 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-1738vsPurchaseEvaluationFeatures.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).