@ -44,7 +44,7 @@ Chuỗi 4 GĐ per-gói-thầu theo SOL-CCM-SP-002: GĐ1 Duyệt NCC (PE = CHA,
| 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 | ⏳ đã giải thích, chờ anh soát | K1 (seed) |
| 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ở |
- Seeder **idempotent** (`existingCodes.Contains(code) continue` + `IgnoreQueryFilters` né #75/#76) ⇒ lần deploy đầu `added=86`, restart sau `added=0`.
- Serilog line dự kiến: `Seeded {Count} contract catalog entries (S164 SP-002)` (chỉ ghi khi `added > 0`).
⇒ Dự đoán KHỚP kỳ vọng đề bài. Giờ mới đo prod.
## Bundle PRE-deploy (đo LIVE lúc run đang chạy, 15:47)
NIT: spec `:28` đặt tên `UX_ContractCatalogEntries_Code`, code để EF-default `IX_ContractCatalogEntries_Code` (`Mig:48`). Không ảnh hưởng chức năng; khuôn repo (Mig 47) cần đối chiếu.
NIT: spec `:28` đặt tên `UX_ContractCatalogEntries_Code`, code để EF-default `IX_ContractCatalogEntries_Code` (`Mig:48`). Không ảnh hưởng chức năng.
### B3 test (nguyên văn)
```
@ -56,3 +55,70 @@ Script parse trực tiếp `DbInitializer.cs` vùng `SeedContractCatalogAsync` (
### B4-d Gate seeder — ĐẠT
`DbInitializer.cs:129-131` call-site NẰM NGOÀI mọi `if (!demoSeedDisabled)` ⇒ UNGATED (đúng spec K1.b, gotcha #51). `IgnoreQueryFilters()` có mặt khi dò existing (né #75/#76 resurrect).
### B4-e MenuKeys + drift DELTA — ĐẠT (1 finding kéo theo ở B4-f)
Đo máy trên `MenuKeys.cs` (parse vùng mảng `All`, strip comment, tách dấu phẩy):
- Grant Admin: loop `SeedAdminPermissionsAsync``DbInitializer.cs:2064-2066` nguồn = `MenuKeys.All` ⇒ tự động, 0 code thêm. Menu row `DbInitializer.cs:1773-1776` leaf order 5 dưới `Catalogs`, đi nhánh INSERT (key mới) nên không dính bẫy gotcha #11.
- 🔴 **Drift DELTA CHƯA vào doc:**`docs/STATUS.md:6` hiện vẫn `Mig 69 · 96 bảng · 590 test PASS · Policies 220 · Menu keys 55`. Acceptance spec-cum1 ③ đòi "drift DELTA khai cùng commit" ⇒ **F-2**.
### B4-f CQRS + Controller authz — ĐẠT-theo-khuôn, nhưng khoảng-hở phải khai
Soi TỪNG action (không nhận `[Authorize]` trần làm bằng chứng):
| Action | Attribute thật | File:line |
|---|---|---|
| class | `[Authorize]` trần | `ContractCatalogController.cs:22` |
Khuôn đối chứng `CatalogsController.cs`: `:13` route · `:14``[Authorize]` trần · 12 action ghi đều `[Authorize(Roles="Admin")]` (`:23,31,40,53,61,70,83,91,100,113,121,130`) ⇒ **mirror khuôn CHÍNH XÁC**, đúng QUYẾT spec K1.c `:47`. **KHÔNG có OR key con** (gotcha #85 N-A: controller này không dùng policy nào).
🔴 Đo tiếp: `grep -rn "ContractCatalog\." src/Backend --include=*.cs | grep -i policy` → **0 endpoint** dùng 4 policy vừa sinh. Hệ quả đo được (lớp gotcha #82): ô CRUD `ContractCatalog` trên ma trận phân quyền chỉ điều khiển **hiển thị menu**, KHÔNG điều khiển API — tick `CanCreate` cho CatalogManager vẫn 403 (`Roles="Admin"`), bỏ tick `CanRead` vẫn GET được (class `[Authorize]` trần). ⇒ **F-3**.
Validator (`ContractCatalogFeatures.cs:110-124` / `:168-183`): MaxLength lấy ĐÚNG từ EF config (20/500/500/10/100/300/500) — đối chiếu `ContractCatalogEntryConfiguration.cs:18-24` khớp 7/7. `ApprovalGroup``InclusiveBetween(1,8)` cả Create lẫn Update. Dup-Code chạy trên active-set (`:131`, `:193`) ⇒ khớp index filtered.
🔸 `ListContractCatalogQuery` (`:42-46`) mặc định **KHÔNG lọc IsActive** ⇒ trả CẢ dòng đã tắt; spec K1.c `:50` ghi tham số `IncludeInactive?` (ngữ nghĩa mặc-định-loại). ⇒ **F-4**.
### B4-g FE 2 app — ĐẠT (đối xứng đo bằng hash)
-`CatalogsPage.tsx` SHA-256 **d71a9e3bb17b835b85b4f3467905af2ab414f52e3ec0c58e79d55ef57673985a**×2 app IDENTICAL → **khớp claim lane FE `sub-implfe-k1.md:49`**.
T1b có RĂNG thật (không phải nhãn): fixture RIÊNG wire `AuditingInterceptor` (`:83`) ⇒ `Remove()` là xoá MỀM thật; có sanity chống-vacuous `raw.IsDeleted.Should().BeTrue()` (`:179`); assert 85 active **VÀ** 86 tổng `IgnoreQueryFilters` (`:185-190`) ⇒ gỡ `IgnoreQueryFilters()` khỏi seeder thì tổng thành 87 và test ĐỎ. Đúng phép đo cho #75/#76.
T6 còn cài invariant TOÀN BỘ `All`: `MenuKeys.All.Except(menuKeysInDb).Should().BeEmpty()` (`:414`) — bắt lỗi key ∈ All mà thiếu MenuItem row (FK nổ lúc boot).
### B5 Ranh CẤM — SẠCH
-`git status --porcelain | grep -cE "ContractSigningPlan"` → **0**.
-`git diff --stat` cho `Infrastructure/Services/ContractSigningPlanWorkflowService.cs` + `Application/Contracts/ContractSigningPlanFeatures.cs` → **rỗng** (0 thay đổi) ⇒ `WorkflowService.cs:342` KHÔNG bị đụng · vùng `Features.cs:342-347` (việc của K2+) KHÔNG bị đụng — đúng vá-2 spec-cum1 `:9`.
- Scope toàn diff: **17 file** (11 M + 6 untracked) = 6 FE + 10 BE + 1 test. 0 file ngoài phạm vi K1.
- 3-file rule Mig (gotcha #17): `20260731072441_AddContractCatalogEntries.cs` + `.Designer.cs` (cả 2 untracked, có trên đĩa) + `ApplicationDbContextModelSnapshot.cs` (modified, +83 dòng, có `ContractCatalogEntry`) = **đủ 3, cùng commit-set**; `grep -c RenameColumn` = **0**.
### B6 Hai khoảng-hở UX lane FE khai — XÁC NHẬN ĐÚNG THỰC TRẠNG (không quyết thay lead/owner)
1.**3 filter BE chưa có UI** — ĐÚNG. BE nhận 4 tham số (`groupCode`/`approvalGroup`/`isActive`/`q` — `Controller:27-30`), FE chỉ gửi `q` (`CatalogsPage.tsx:158`) ⇒ 3/4 filter chưa có đường bấm từ UI.
2.**Tab hiện cả fe-user** — ĐÚNG. `KINDS` render vô điều kiện 5 sub-tab, route `:kind` mở ở CẢ 2 app (`fe-user/src/App.tsx:63`), GET là `[Authorize]` trần ⇒ user thường vào `/master/catalogs/units` sẽ thấy tab "Danh mục Hợp đồng" và đọc được 86 dòng, dù leaf menu chỉ Admin được grant. Cùng lớp với F-3.
## FINDING (9 — 0 blocker kỹ-thuật; F-1 chờ lead/owner disposition trước khi commit seed)
1.**[HIGH · gate quy-trình]** `DbInitializer.cs:3147-3324` — 86 dòng seed đang nằm CÙNG change-set với code, trong khi spec đặt PRE-gate `[OG-6]` (owner soát transcribe) **TRƯỚC khi land seed** và kế hoạch tách 2 commit (`sub-invest-fable-b2-cum1.md:7` + `:240-241`: "Commit K1-seed (SAU [OG-6] anh gật)"). Nặng thêm vì seeder per-Code idempotent **không có nhánh update**: owner sửa ô sau khi seed đã chạy thì row cũ KHÔNG tự cập nhật (lane tự khai `sub-implbe-k1.md:51`). Tiêu chí đóng: hoặc owner gật OG-6 trước commit, hoặc tách seed sang commit riêng giữ chờ gật.
2.**[MEDIUM]** `docs/STATUS.md:6` — drift DELTA chưa vào doc: Mig 69→**70** · 96→**97** bảng · 590→**597** test · Policies 220→**224** · Menu keys 55→**56**. Acceptance spec-cum1 ③ đòi khai **cùng commit**.
3.**[MEDIUM · lớp #82]** `ContractCatalogController.cs:22,39,47,56` — 4 policy `ContractCatalog.{Read,Create,Update,Delete}` sinh ra do key vào `All` nhưng **0 endpoint tiêu thụ**; ô CRUD trên ma trận phân quyền chỉ điều khiển hiển-thị-menu, không điều khiển API (tick CanCreate vẫn 403 · bỏ tick CanRead vẫn GET được). KHÔNG phải lệch spec (K1.c QUYẾT mirror khuôn `CatalogsController.cs:14`) nhưng phải khai thành giới-hạn, đừng để lượt sau đọc ma trận thành "đã khoá API".
4.**[LOW]** `ContractCatalogFeatures.cs:42-46,59-60` — List mặc định trả CẢ dòng `IsActive=false`; entity khai mục đích cột này là "ẩn khỏi picker" (`ContractCatalogEntry.cs:23`) và spec K1.c `:50` đặt tham số `IncludeInactive?` (mặc-định-loại). Ngữ nghĩa hiện tại ngược ⇒ consumer K2 (picker) phải nhớ truyền `isActive=true`, quên là dòng đã tắt hiện lại.
5.**[LOW]** `fe-{admin,user}/src/pages/master/CatalogsPage.tsx` — 0 hit `PermissionGuard`/`usePermission`: nút Thêm/Sửa/Xóa render cho mọi user vào được trang, bấm mới nhận 403. Pre-existing của khuôn 4 kind cũ, không phải hồi quy do K1.
6.**[NIT]** `20260731072441_AddContractCatalogEntries.cs:48` — tên index `IX_ContractCatalogEntries_Code` (EF default) trong khi spec `sub-invest-fable-b2-cum1.md:28` đặt `UX_ContractCatalogEntries_Code`. Chức năng không đổi (unique + filter đúng).
7.**[NIT]** `fe-{admin,user}/src/pages/master/CatalogsPage.tsx:218` — comment `{/* Sub-tabs cho 4 kind */}` còn "4" trong khi `KINDS` đã 5 kind.
8.**[INFO · lệch đã khai]** `DbInitializer.cs` B2-07..B2-14 — 8 dòng bỏ hậu tố `(ảnh: …)` so với transcribe; đo máy xác nhận **đây là 8 mismatch DUY NHẤT** trên 86 dòng, đúng tập lane đã khai (`sub-implbe-k1.md:16`). Bản chất là ghi-chú người chép, nhưng vẫn thuộc phạm vi OG-6 owner soát.
9.**[LOW]** Acceptance K1.f `:70-71` (runtime) chưa đo: Mig 70 chưa áp Dev-DB (lane khai `sub-implbe-k1.md:48-49`), chưa có log "Seeded 86…" + `sqlcmd COUNT(*)=86` + `GROUP BY ApprovalGroup`. T1/T1b thay được phần logic-idempotent trên SQLite nhưng KHÔNG thay được "SQL Server nhận đúng filtered index + boot thật" ⇒ đừng tick 2 ô đó cho tới khi chạy API.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.