From e695f8aeaa5d87c50036be24d9d1377962cfe869 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 31 Jul 2026 12:45:46 +0700 Subject: [PATCH] wal: flush 20260731T1245 --- .claude/WAL.md | 11 +- .../2026-07-31-S164-4gd-khkk-fanout/run.md | 3 +- .../spec-4gd-khkk-tong-quat-31-07-2026.md | 2 +- .../sub-invest-fable-b2-cum1.md | 250 +++++++++++++++++- .../sub-reviewer-c1-l1.md | 12 + .../sub-reviewer-c1-l2.md | 12 + .../sub-reviewer-c1-l3.md | 13 + 7 files changed, 293 insertions(+), 10 deletions(-) create mode 100644 .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md create mode 100644 .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md create mode 100644 .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 50d9976..b8040ad 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -26,9 +26,14 @@ next: 5/6 OG CHỐT (1 schema-GỘP+UI-1-1 · 2 đội-hình-cũ · 7 N-phiếu+ sub-invest-fable-b2-cum1.md, skeleton-first) → về thì: verify đĩa → merge note OG-3-finalize vào spec chi-tiết → kết tinh → fable-clone review cụm-1. Nguyên tắc mới memory: "máy rộng, UI hẹp". 🆕 QUYẾT ĐỊNH 9 (owner): KHÓA BUDGET tại finalize (a Chương TP.CCM) = mirror PE Mig 67 + né #81-EXT — spec §②.9. - 🔴 B2c1 att-1 wf_fe53e58b-361 CHẾT skeleton-ruột-rỗng 472B (đọc 152K, 0 Edit — đọc-hết-mới-viết). att-2 - wf_0b6b511f-76d ĐANG CHẠY: luật ĐAN XEN đọc→viết per section + trần đọc + quyết-định-9 đã nhét vào prompt. - Pattern ghi sổ: tier:fable × StructuredOutput fail 3/3 (opus 3/3 sạch) — B3 cân nhắc. + B2c1 att-1 skeleton-472B chết · att-2 wf_0b6b511f-76d ✅ SỐNG TRỌN (33.048B 5/5 section 17 test, luật + ĐAN-XEN cứu; return CŨNG sạch). 3 QUYẾT spot-check 3/3 ĐẠT: freeze = 0-cột-mới (máy Line+??= sẵn, chỉ + thêm răng #81-EXT site :342) · catalog authz mirror CatalogsController · MÂU THUẪN OG-3 THẬT (BE chưa + port finalize — KHKK_EARLY_FINALIZE_NOTE system/ApprovalWorkflowsV2Page:151) → spec §④ OG-3 ĐÃ SỬA + (K3 3 việc a/b/c). Roster ô ⚠️ "Trưởng" 3 ứng viên — verify LIVE trước seed. + wf: B2-r cụm-1 wf_2a967a21-79e ĐANG CHẠY (3 lane reviewer opus: c1-evi · c1-cov · c1-seq). + CHỜ ANH: câu (I) bản-15-trang quy-trình-chữ khác bản-6-trang? · (II) B2-14-dòng + "/"=hoặc · (III) ai bấm + "Đưa vào HĐ"+chọn form. Pattern sổ: fable-lane StructuredOutput-fail 3/4 (att-2 này SẠCH — n=4). verify: grep -c 'END spec v2' .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md index 0f97201..462eb5f 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md @@ -10,7 +10,8 @@ | 0 | Draft yêu cầu owner | em-main | ✅ `draft-yeu-cau-owner.md` | | 1a | Spec tổng quát các wave | /fable-real invest | ✅ att-2 (att-1 ngược-#53 0-byte; att-2 A-D đầy 18.277B, E/F lead-compile; spot-check 4/4) | | 1b | Review spec tổng quát — 3 lane `WF reviewer lens{1,2,3} s164` | /fable-clone review (tier:opus ×3) | ✅ 3/3 sạch 0-empty · 41 finding thô → synthesis 14 vá + 2 OG mới → **spec v2** | -| 2.x | Per cụm 3 wave: spec chi tiết + checklist | /fable-real invest (1 con/cụm) | ☐ | +| 2.c1 | Cụm-1 K1+K2+K3 chi tiết — `WF invest-fable b2c1-att2 s164` | /fable-real invest | ✅ att-2 33.048B 5/5 section 17 test (att-1 skeleton-472B chết; luật ĐAN-XEN cứu) — 3 QUYẾT + 1 mâu-thuẫn OG-3 (K3 phải port finalize) + roster 1 ô ⚠️ Trưởng | +| 2.c2/c3 | Cụm-2 K4+K5+K6 · cụm-3 K7+K8 chi tiết | /fable-real invest | ☐ | | 2.x-r | Review spec chi tiết per cụm | /fable-clone review | ☐ | | 3 | Chốt spec cuối | /fable-real review | ☐ | | 4 | Fan-out build bám spec+checklist | Opus 5 MAX (hmw) | ☐ | diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md index 542c69a..f936149 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md @@ -43,7 +43,7 @@ Chuỗi 4 GĐ per-gói-thầu theo SOL-CCM-SP-002: GĐ1 Duyệt NCC (PE = CHA, | 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) — K3 **0 code mới** cho nhánh này, CHỈ config workflow 8 nhóm có cấp CCM mang quyền finalize | — | +| 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-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 | diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md index fb8c4a1..8b09b9f 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md @@ -3,16 +3,256 @@ > Engine /fable-real single deep-pass — ATT-2 (att-1 chết skeleton-ruột-rỗng 472B, đọc 152K chưa viết gì). Luật sắt att-2: ĐAN XEN đọc→viết per section. Nguồn pin: spec v2 §② 9 quyết định + danh-muc-sp002-transcribe (86 row) + khuôn code sống (DbInitializer / ContractSigningPlan* / PE Mig 67). ## K1 Master-data danh mục (ContractCatalogEntries + Mig 70 + seed 86 + CQRS + FE ×2) -(ĐANG LÀM) + +**PRE-gate:** [OG-6] anh soát `danh-muc-sp002-transcribe.md` (4 ô ⚠️ cuối file) TRƯỚC khi land seed. Code/schema land trước được, seed-data commit sau gật. + +### K1.a — Entity + schema bảng `ContractCatalogEntries` + +Entity `ContractCatalogEntry : AuditableEntity` (soft-delete — mirror khuôn 4 catalog Master sẵn) tại `src/Backend/SolutionErp.Domain/Master/ContractCatalogEntry.cs` (cạnh WorkItem — folder Domain/Master hiện hữu). + +| Cột | Type SQL | Null | Ghi chú | +|---|---|---|---| +| `Id` | uniqueidentifier PK | NOT NULL | BaseEntity | +| `Code` | nvarchar(20) | NOT NULL | `A1-01`…`CEO-01` — **UNIQUE filtered `WHERE [IsDeleted]=0`** (#57, khuôn Mig 47) | +| `TenVi` | nvarchar(500) | NOT NULL | tên VN (dài nhất A3-09 ~150 ký tự) | +| `TenEn` | nvarchar(500) | NULL | tên EN (seed đủ 86; nullable cho row tay sau) | +| `GroupCode` | nvarchar(10) | NOT NULL | `A1..A4, B1..B3, C1..C3, CEO` — section SP-002 | +| `ApprovalGroup` | int | NOT NULL | **1..8** — map N1..N8 (validator range, KHÔNG DB CHECK — khuôn repo không dùng CHECK) | +| `SignerRole` | nvarchar(100) | NOT NULL | TEXT vai ký chính (QĐ-4 2-tầng: TEXT in GĐ4 ⟂ Levels người thật) | +| `InitialsChain` | nvarchar(300) | NULL | chuỗi ký nháy `TBP.CCM→NV.CCM→TBP.EQU→NV.EQU` (Ô1→Ô4, bỏ N/A) | +| `NduqNote` | nvarchar(500) | NULL | tham khảo NĐUQ [OG-3] — seed **null** (ảnh không có số per-dòng, ⚠️3 transcribe) | +| `IsActive` | bit | NOT NULL def 1 | ẩn khỏi picker không cần xoá (khuôn WorkItem Mig 49 validator IsActive) | +| `SortOrder` | int | NOT NULL def 0 | thứ tự trong nhóm (seed = STT trong section) | +| audit 7 cột | — | — | CreatedAt/UpdatedAt/CreatedBy/UpdatedBy + IsDeleted/DeletedAt/DeletedBy | + +Index: `UX_ContractCatalogEntries_Code` UNIQUE filtered `[IsDeleted]=0` · `IX_ContractCatalogEntries_ApprovalGroup` non-unique (picker K2 lọc theo nhóm phiếu). +Config `Persistence/Configurations/ContractCatalogEntryConfiguration.cs`: ToTable + HasQueryFilter(!IsDeleted) + 2 index + MaxLength như bảng. DbSet vào `ApplicationDbContext` + `IApplicationDbContext`. + +**Mig 70 `AddContractCatalogEntries`** — 1 CreateTable, bảng **96→97**. 3-file rule (Up/Down + Designer + Snapshot — gotcha #17). Down = DropTable (reversible, chưa ai tham chiếu tại K1; FK từ Lines là chuyện Mig 71). + +### K1.b — Seed `SeedContractCatalogAsync` (86 row, per-Code idempotent, UNGATED) + +- **Gate: UNGATED** — mirror `SeedRealMasterDataAsync` (`DbInitializer.cs:2893-2896` "INFRASTRUCTURE master data thật... NOT gated DemoSeed") — danh mục SP-002 là master data thật phải có mặt prod ngay deploy, không phải demo. +- **Né #75/#76 (resurrect):** lookup existing codes PHẢI `IgnoreQueryFilters()`: + `var existing = await db.ContractCatalogEntries.IgnoreQueryFilters().Select(c => c.Code).ToListAsync();` + (⚠️ khuôn `SeedRealMasterDataAsync:2970` dò `db.Projects.Select(p=>p.Code)` KHÔNG IgnoreQueryFilters ⇒ chính là lỗ #75/#76 — row soft-deleted vô hình → RE-CREATE mỗi restart. K1 làm ĐÚNG hơn khuôn, ghi comment lý do. Đây là hit `IgnoreQueryFilters` đầu tiên trong src/Backend — S155 đo 0-hit — cố ý, khai trong PR.) +- Foreach 86 tuple `(Code, TenVi, TenEn, GroupCode, ApprovalGroup, SignerRole, InitialsChain, SortOrder)` hardcode inline (khuôn :2904 — runtime không đọc file ngoài) → skip-if-contains else Add → 1 SaveChanges + log count. +- **Mapping transcribe → field:** cột `Mã`→Code · `Tên VN`→TenVi · `Tên EN`→TenEn · heading section (A1..C3, 5.2)→GroupCode · cột `→ Nn` heading→ApprovalGroup · bảng "Nhóm duyệt (8)" đầu file: `Ký chính`→SignerRole, `Nháy Ô1→Ô4` (bỏ N/A)→InitialsChain — **denorm per-row** (86 row cùng nhóm chép cùng chuỗi; QĐ-4 chấp nhận TEXT). STT trong section→SortOrder. +- Đếm theo nhóm (khớp transcribe): N1=5 · N2=10 (A2 1+A3 9) · N3=5 · N4=23 · N5=14 (⚠️1 B2 đánh lại 01-14) · N6=15 · N7=13 (C1 6+C2 3+C3 4) · N8=1 → **Σ=86**. +- Gọi trong `InitializeAsync` cạnh `SeedRealMasterDataAsync` (trước sample workflows). + +### K1.c — BE CQRS + controller (QUYẾT policy: mirror CatalogsController, KHÔNG policy mới) + +**SOI MẪU:** `Api/Controllers/CatalogsController.cs:13-14` = `[Route("api/catalogs")]` + class `[Authorize]` (đăng nhập = đọc được); 12 action write đều `[Authorize(Roles = "Admin")]` (:23-:130). Grep `Authorize(Policy = "(WorkItems|MaterialItems|UnitsOfMeasure|ServiceItems)` toàn Controllers = **0 hit** ⇒ "Catalogs.*"-policy per-action KHÔNG tồn tại trong khuôn catalog. +**QUYẾT:** mirror y khuôn — GET `[Authorize]` thừa hưởng class, 4 mutation `[Authorize(Roles = "Admin")]`. Lý do: (i) luật "GIỐNG NHAU — ĐỪNG CHẾ THÊM"; (ii) picker tạo phiếu K2 cần READ rộng mọi user KHKK — nếu khoá `KeHoachKyKet.Read` thì user thiếu grant leaf sẽ 403 picker (#44/#85); (iii) `KeHoachKyKet.Update` (drafter sửa phiếu) KHÔNG được phép sửa danh mục — sai vai; (iv) OG-8 "phân quyền tính sau" — 0 policy/menu-key mới = 0 drift STATUS row Policies. + +- CQRS: file MỚI `Application/Master/Catalogs/ContractCatalogFeatures.cs` (cạnh `CatalogsFeatures.cs` hiện hữu — mega-file 1 file, Pattern 12-ter): + `ListContractCatalogQuery {ApprovalGroup?, GroupCode?, Search?, IncludeInactive?}` → all-rows sort (GroupCode, SortOrder), KHÔNG paging (86 row; picker + trang admin dùng chung) · `GetContractCatalogEntryQuery {Id}` · `CreateContractCatalogEntryCommand` + `UpdateContractCatalogEntryCommand` (validator: NotEmpty Code/TenVi/GroupCode/SignerRole; `ApprovalGroup` InclusiveBetween(1,8); dup-Code active-set → ConflictException — so exact `Code ==`, filtered-unique cho phép trùng với row đã xoá mềm) · `DeleteContractCatalogEntryCommand` soft-delete (Line K2 giữ FK Restrict chặn hard-delete; soft-delete OK vì Line denorm `TenHangMuc`). +- Controller: **EXTEND `CatalogsController`** +5 action `GET/POST/PUT/DELETE api/catalogs/contract-catalog[/{id}]` — không controller mới (khuôn 4 kind sẵn trong 1 controller). + +### K1.d — FE ×2 app (KIND_CONFIG kind mới) + +- File mẫu: `fe-admin/src/pages/master/CatalogsPage.tsx` + `fe-user/src/pages/master/CatalogsPage.tsx` (KIND_CONFIG `Record` theo `:kind` URL — khuôn S29/S35). +- Thêm kind `contract-catalog` vào KIND_CONFIG ×2 app: columns {Code, TenVi, TenEn, GroupCode, ApprovalGroup (badge N1..N8), SignerRole, InitialsChain, IsActive, SortOrder}; form create/edit đủ field; endpoint `api/catalogs/contract-catalog`. Duplicate 2 app CÓ CHỦ ĐÍCH (CLAUDE.md). +- Menu leaf: +const `MenuKeys.ContractCatalog` (**NGOÀI `All`** — khuôn leaf `Pe_*`/tiền lệ `HopDongCung` NGOÀI All = 0 policy mới; hiển thị đi đường Permission row) + 1 MenuItem row con group `Catalogs` trong `SeedMenuTreeAsync` + grant admin (SeedAdminPermissionsAsync loop) + mirror `fe-{admin,user}/src/lib/menuKeys.ts`. Route `:kind` = `contract-catalog` — 0 route file mới. + +### K1.e — Test (k=5) + +1. `Seed_ContractCatalog_86Rows_Idempotent` — chạy seed ×2 → count active == 86, không nhân row. +2. `Seed_SkipsSoftDeletedCode_NoResurrect` — soft-delete `A1-01` → re-seed → active == 85, row không hồi sinh (#75/#76 có răng). +3. `Create_DuplicateActiveCode_Throws409_ButAllowsReuseOfSoftDeletedCode` — dup active → Conflict; Code trùng row IsDeleted=1 → OK (filtered #57 2 chiều). +4. `CreateOrUpdate_ApprovalGroupOutOfRange_FailsValidation` — 0 và 9 fail, 1 và 8 pass. +5. `Delete_IsSoft_ListExcludes_RowRemains` — Delete → list 85, row DB IsDeleted=1. + +### K1.f — Checklist acceptance K1 + +- [ ] `dotnet ef migrations add AddContractCatalogEntries …` → đủ **3 file** trong `Persistence/Migrations/`; `git status` show 3. +- [ ] Restart API → log "Seeded 86 contract catalog entries"; `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "SELECT COUNT(*) FROM ContractCatalogEntries WHERE IsDeleted=0"` → **86**; `SELECT ApprovalGroup, COUNT(*) … GROUP BY ApprovalGroup` → 5/10/5/23/14/15/13/1. +- [ ] Restart API lần 2 → vẫn 86 (idempotent by-restart-THẬT, khuôn S91). +- [ ] `curl -H "Authorization: Bearer " localhost:5443/api/catalogs/contract-catalog` → 200, 86 item; POST thiếu role Admin → 403. +- [ ] `dotnet test SolutionErp.slnx` → baseline (STATUS.md row Tests, hiện ~590) **+5**, khai k=5. +- [ ] DoD spec: N dòng ~85±5 khớp — **đúng 86**, lệch 0; tên VN+EN đủ 86/86. +- [ ] STATUS.md: bảng 96→**97** + Mig 69→**70**; schema-diagram migration table +row 70. ## K2 Phiếu hạng-mục + nhóm (Mig 71: ApprovalGroup + CatalogEntryId + rào (PeId,Group) + budget-freeze QĐ-9) -(ĐANG LÀM) + +**PRE-gate:** [OG-1] + [OG-7] — cả 2 ✅ CHỐT @S164 (spec §④). Phụ thuộc K1 (bảng catalog phải có trước để trỏ). + +### K2.a — Mig 71 `AddContractSigningPlanGroupAndLineCatalog` (3-file rule, no new table) + +1. `ContractSigningPlans` **+`ApprovalGroup int NOT NULL, defaultValue: 1`** — AddColumn với default tự backfill mọi row hiện có = QĐ-6 backfill=1 (khuôn `EvaluationType` l3-F5; KHÔNG cần `Sql()` riêng — default áp cả Design-DB 0-row lẫn prod-DB có-row, né #64 lệch môi trường). +2. `ContractSigningPlanLines` **+`CatalogEntryId uniqueidentifier NULL`** — **loose-Guid KHÔNG FK vật lý** (QUYẾT: mirror y Mig 49 `PE.WorkItemId` "convention PE: ProjectId/SelectedSupplierId đều loose"; chính Line.cs:15 `SupplierId` đã loose cố ý — thêm FK thật là phá khuôn module) + `IX_ContractSigningPlanLines_CatalogEntryId`. +3. `ContractSigningPlanLines` **+`TenHangMuc nvarchar(500) NULL`** — denorm tên tại thời điểm gán (catalog rename sau KHÔNG viết lại lịch sử phiếu — khuôn `SignedByFullName` denorm Mig 26). +4. **Đổi UNIQUE:** DropIndex unique cũ trên (`ContractSigningPlanId`,`SupplierId`) (tên EF-sinh — lấy đúng từ `ContractSigningPlanLineConfiguration.cs` vùng :24-26 khi build) → CreateIndex **UNIQUE (`ContractSigningPlanId`,`SupplierId`,`CatalogEntryId`) filtered `WHERE [IsDeleted]=0`**. + - ⚠️ **Bẫy NULL-unique SQL Server:** trong UNIQUE index, NULL == NULL ⇒ 2 row cùng (Plan,Supplier) đều `CatalogEntryId NULL` sẽ VI PHẠM. Data cũ AN TOÀN (unique cũ bảo đảm ≤1 row/cặp ⇒ tối đa 1 NULL-row/cặp). Chặn phát sinh mới bằng validator: **line MỚI/SỬA bắt buộc có `CatalogEntryId`** (chỉ row legacy được giữ null). KHÔNG thêm `AND CatalogEntryId IS NOT NULL` vào filter — làm vậy là thả trôi vô hạn dup (Plan,Supplier,null). +5. Config update cùng file `ContractSigningPlanLineConfiguration.cs`: 2 property + index swap; `:29 WithMany(p => p.Lines)` giữ nguyên. + +### K2.b — QĐ-9 budget-freeze: QUYẾT **per-line, TÁI DÙNG `ApprovedAmount` — 0 cột tiền mới, KHÔNG chép 11 cột PE Mig 67** + +**Bằng chứng máy-freeze ĐÃ TỒN TẠI:** +- `ContractSigningPlanLine.cs:17-20` — `PeReferenceAmount` = SNAPSHOT chụp lúc TẠO, comment tự khai *"cùng triết lý freeze ngân sách Mig 67"*; `:23 ApprovedAmount` — *"CHỈ ghi tại choke-point finalize (W3)"*. +- `ContractSigningPlanWorkflowService.cs:304` — `line.ApprovedAmount ??= line.ProposedAmount;` foreach `:303` chạy tại finalize; `:290` comment cảnh báo đúng bài #81-EXT: *"Bỏ sót một nhánh = phiếu 'Đã duyệt' mà ApprovedAmount rỗng ⇒ GĐ3 sinh hợp đồng 0đ"*. +- Grep `PeWorkItemBudget|WinnerQuoteTotal` trong `ContractSigningPlan*` = 1 hit label (`Features.cs:162` DTO picker) — **KHKK KHÔNG đọc bảng ngân sách live nào**. PE Mig 67 cần 11 cột vì PE detail đọc LIVE `PeWorkItemBudgets`; KHKK mọi số đã là cột local của chính nó. +**⇒ Lý do QUYẾT:** freeze = có sẵn về SCHEMA; K2 chỉ thêm RĂNG + GATE (luật "ĐỪNG CHẾ THÊM"). Header KHÔNG cần cột: `TotalPeReferenceAmount` (list DTO `:53`, Σ `:711`) derive từ lines bất biến. + +**Việc phải làm (răng + gate):** +- **#81-EXT sweep — bảng Phase-assignment-site KHKK** (grep `Phase\s*=[^=]` — chú ý KHÔNG grep `\.Phase` vì object-initializer `Features.cs:373` không có dấu chấm, thoát lưới hẹp): + | Site | Gán | Freeze? | + |---|---|---| + | `Features.cs:373` | `Phase = DangSoanThao` (create initializer) | không cần (không vào DaDuyet) | + | `WorkflowService.cs:174` | `= ChoDuyet` (submit) | không cần | + | `WorkflowService.cs:278` | `= DaDuyet` (finalize choke-point) | ✅ đã gọi hook `:303-304` | + | `WorkflowService.cs:342` | `= targetPhase` (gán qua BIẾN — đúng class bẫy `:39` comment) | 🔴 PHẢI XỬ: nếu nhánh này reachable với `targetPhase == DaDuyet` (admin-override/return-path) thì hook `:304` bị bypass ⇒ hoặc (i) gọi `ApplyApprovedAmounts` ngay trước `:342` khi `targetPhase == DaDuyet`, hoặc (ii) assert nhánh không nhận DaDuyet + test khoá hành vi. Mirror đúng bài PE Mig 67 site-5 admin-override. | + Acceptance: re-grep khi build, liệt ĐỦ site trong PR (seeder/DbInitializer hiện 0 site gán phase KHKK). +- **Display-gate `frozen`:** DetailDto +`budgetFrozen = phase == DaDuyet` (khuôn tên PE S133); FE DaDuyet đọc `ApprovedAmount` + badge khoá, trước đó đọc `ProposedAmount`; mọi editor line (K4b) chỉ mở ở Nháp/TraLai — sau DaDuyet cấm sửa/xoá line (xoá mềm line sau DaDuyet làm Σ `:711` trôi ÂM THẦM — đây là leak gián tiếp duy nhất, chặn bằng phase-guard). + +### K2.c — Đổi rào 1-PE-1-phiếu → (PeId, ApprovalGroup) [OG-7] — 2 site + +- **Site 1 create `Features.cs:342-347`:** `AnyAsync(p => p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)` → **+`&& p.ApprovalGroup == request.ApprovalGroup`**; message mới: *"Phiếu Duyệt NCC này đã có kế hoạch cho NHÓM duyệt này. Chọn nhóm khác hoặc xoá kế hoạch cũ."* `CreateContractSigningPlanCommand` +`ApprovalGroup int` (validator `InclusiveBetween(1,8)`); create gán `plan.ApprovalGroup = request.ApprovalGroup`. +- **Site 2 picker `Features.cs:1201-1202`** (query PE-eligible): bỏ loại-trừ PE-đã-có-phiếu → trả kèm **`existingGroups: int[]`** per PE (groups của các plan sống). **UI hướng-1 (OG-7 verbatim):** PE đã có phiếu → vẫn hiện, hiển thị phiếu hiện có + nút phụ **"Tạo thêm cho nhóm khác"** (disable các nhóm ∈ existingGroups trong select nhóm) — thay 409 chặn hẳn. FE `KhkkCreatePage` ×2 app + `types/khkk.ts` DTO picker. +- Auto-gen Lines `:389` giữ per-winner, mồi `CatalogEntryId = null, TenHangMuc = null` (gán hạng mục = Lines-editor K4b, KHÔNG thuộc K2 — create hiện *"FE KHÔNG gửi lines"* `KhkkCreatePage.tsx:8`). + +### K2.d — Validator line-cùng-nhóm (code mẫu) + +Chạy ở 2 chỗ: upsert-lines (K4b, fail-fast) + **submit-guard** (service, chống data cũ lách): +```csharp +var ids = plan.Lines.Where(l => l.CatalogEntryId != null) + .Select(l => l.CatalogEntryId!.Value).Distinct().ToList(); +var badGroup = await db.ContractCatalogEntries + .AnyAsync(c => ids.Contains(c.Id) && c.ApprovalGroup != plan.ApprovalGroup, ct); +if (badGroup) throw new ConflictException( + "Có hạng mục không thuộc nhóm duyệt của phiếu — mọi dòng phải cùng nhóm N" + plan.ApprovalGroup); +``` +Line `CatalogEntryId == null` (legacy nhóm-1 backfill) → CHO QUA (không chặn phiếu cũ resubmit). + +### K2.e — List-query + DTO + bất-biến Σ + +- `ListContractSigningPlansQuery` (`Features.cs:622-626`) +`ApprovalGroup int? = null` → `if (request.ApprovalGroup is not null) q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup);` — **K2 là CHỦ param này** (QĐ-3/l3-F6; K4b leaf + K5 cây chỉ TIÊU THỤ). +- `ContractSigningPlanListItemDto` (record vùng `:34-53`) +`ApprovalGroup` · `DetailDto :608-616` +`ApprovalGroup` +`BudgetFrozen` · `LineDto :570-574` +`CatalogEntryId` +`TenHangMuc` · FE `types/khkk.ts` mirror ×2 app. +- **Bất-biến Σ PeReferenceAmount:** Mig 71 KHÔNG đụng data tiền ⇒ `SELECT SUM(PeReferenceAmount) FROM ContractSigningPlanLines` TRƯỚC == SAU migrate (acceptance đo 2 lần). Redefine semantics (QĐ-2b): row legacy = tham chiếu per-NCC-toàn-phiếu (giữ nguyên); row MỚI cùng NCC (tách hạng mục, K4b) mồi `PeReferenceAmount = 0` ⇒ **Σ per-(phiếu,NCC) == số PE gốc bất biến khi tách dòng**; khai trong doc + tooltip FE. + +### K2.f — Bảng consumer `.Lines` ĐÃ-XỬ / KHÔNG-ĐỔI (grep-verified; đối chiếu danh sách 16 = 10 BE + 6 FE của l3-F18 `b1b-review-synthesis.md`) + +| # | Site | Disposition | +|---|---|---| +| 1 | `WorkflowService.cs:111` Include Lines | KHÔNG-ĐỔI (Include tự nạp cột mới) | +| 2 | `WorkflowService.cs:296` comment scope | KHÔNG-ĐỔI (doc) | +| 3 | `WorkflowService.cs:303-304` foreach freeze | **XỬ** — thêm răng site-`:342` (K2.b) | +| 4 | `LineConfiguration.cs:29` WithMany | KHÔNG-ĐỔI; cùng file **XỬ** +2 property + index swap | +| 5 | `Features.cs:389` auto-gen create | **XỬ** — mồi 2 field null + comment semantics | +| 6 | `Features.cs:412` changelog count | KHÔNG-ĐỔI | +| 7 | `Features.cs:417` result count | KHÔNG-ĐỔI | +| 8 | `Features.cs:500` Include detail | KHÔNG-ĐỔI | +| 9 | `Features.cs:524` supplierIds | KHÔNG-ĐỔI | +| 10 | `Features.cs:568-574` projection LineDto | **XỬ** — +2 field | +| 11 | `Features.cs:711` Σ TotalPeReference (list) | KHÔNG-ĐỔI (bất-biến K2.e giữ nghĩa) | +| 12-13 | `types/khkk.ts:203` ×2 app | **XỬ** — `KhkkLineDto` +2 field optional | +| 14-15 | `KhkkDetailPage.tsx:425-441` ×2 app | **XỬ** — +cột "Hạng mục" (TenHangMuc, fallback "—") | +| 16-17 | `KhkkCreatePage.tsx:8` ×2 app | KHÔNG-ĐỔI đợt này (create không gửi lines; editor = K4b) | + +(17 site đếm được > "16" của l3-F18 vì `:296` comment + `:711` — dư an toàn hơn thiếu; PR đối chiếu từng dòng.) + +### K2.g — Test (k=7) + +1. `Mig71_BackfillsApprovalGroup1_CountEqualsSumOfFolders` — mọi plan cũ group==1. +2. `Create_SamePeSameGroup_409_SamePeOtherGroup_201` — rào mới 2 chiều (DoD "phiếu nhóm 2 CÙNG PE → 201"). +3. `Lines_UniqueTriple_DupCatalog_Throws_DiffCatalog_Ok` — cùng (plan,supplier,catalogA) ×2 fail; +catalogB pass (năng lực mới). +4. `Submit_LineWrongGroup_409_LegacyNullCatalog_Pass` — validator K2.d 2 chiều. +5. `Finalize_FreezesAllLines_NoNullApprovedAmount` — sau DaDuyet mọi line ApprovedAmount != null (`:303-304`). +6. `TransitionViaVariableSite_CannotReachDaDuyetWithoutFreeze` — răng site-`:342` (theo phương án (i)/(ii) đã chọn). +7. `Sum_PeReferenceAmount_InvariantAcrossMigAndSplit` — Σ trước/sau + tách dòng mồi 0. + +### K2.h — Checklist acceptance K2 + +- [ ] Mig 71 đủ 3 file; `sqlcmd … "SELECT SUM(PeReferenceAmount) FROM ContractSigningPlanLines"` trước & sau update → **bằng nhau** (ghi 2 số vào PR). +- [ ] `sqlcmd … "SELECT COUNT(*) FROM ContractSigningPlans WHERE ApprovalGroup=1"` == tổng phiếu hiện có; COUNT(tất cả) == Σ COUNT 8 nhóm. +- [ ] curl create phiếu nhóm 2 trên PE đã có phiếu nhóm 1 → **201**; lặp nhóm 1 → **409**. +- [ ] curl list `?approvalGroup=2` → chỉ phiếu nhóm 2; DTO có `approvalGroup`. +- [ ] Grep `Phase\s*=[^=]` toàn `ContractSigningPlan*` → liệt đủ site vào PR (đối chiếu bảng K2.b). +- [ ] `dotnet test` +7, khai k=7; bảng K2.f dán vào PR từng dòng (bài S119 disposition TỪNG DÒNG, không số tổng). ## K3 8 workflow KHKK-N1..N8 + designer + validator pin-đúng-nhóm -(ĐANG LÀM) + +**PRE-gate:** [OG-2] ✅ "Đúng đội hình cũ" — nhưng PHẢI **verify đội hình `QT-DN-V2-001` bản LIVE** trước seed (SSH+sqlcmd prod chết khi SQL-client load — S134/S148 ⇒ đo qua API prod `GET /api/approval-workflows-v2` bằng curl + token admin; Dev làm fallback, flag rõ trong PR nếu prod bất khả). + +### K3.⚠️ MÂU THUẪN SPEC↔CODE phải xử TRƯỚC (finding chính của lane) + +Spec §④ OG-3 ghi *"K3 **0 code mới** cho nhánh này — CHỈ config workflow"* (tái dùng level-finalize V2). Nhưng code THẬT: `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:151-152` — `KHKK_EARLY_FINALIZE_NOTE = 'Chưa hỗ trợ cho quy trình Kế hoạch ký kết HĐ — backend chưa xử lý đường "duyệt sớm", tick vào sẽ KHÔNG có tác dụng.'` (Designer KHOÁ 2 ô duyệt-sớm cho type-10) + `ContractSigningPlanWorkflowService.cs` chỉ có finalize thường `:278` (KHÔNG có nhánh AllowApproverFinalize; MEMORY-verified `:44` comment không port CeoApprovalThreshold). Cột `AllowApproverFinalize` trên `ApprovalWorkflowLevels` CÓ SẴN (Mig 58 — dùng chung bảng, 0 mig) — cái thiếu là NHÁNH SERVICE + mở khoá Designer. +**⇒ K3 PHẢI gồm 3 việc code (không phải 0):** (a) port nhánh duyệt-sớm vào `ContractSigningPlanWorkflowService` — approver tại level `AllowApproverFinalize=true` tick "kết thúc tại đây" → nhảy thẳng choke-point `:278` (**BẮT BUỘC đi qua `ApplyApprovedAmounts :303-304`** — nối răng K2.b, không tự gán Phase tay #81-EXT) + ghi Changelog ContextNote marker `"Duyệt KẾT THÚC tại %"` (khuôn PE Mig 61/#79); (b) +cột runtime `ContractSigningPlans.EndedByLevelFinalize bit NOT NULL DEFAULT 0` — display đọc RUNTIME không suy CONFIG (né tái diễn #78) — **gắn vào Mig 71 item 6 (K2.a) nếu K2 chưa land; nếu đã land → Mig 72 AddColumn riêng**; (c) fe-admin GỠ khoá 2 ô + note `:151-152` cho type-10 (không gỡ = config-lie NGƯỢC: BE hỗ trợ mà UI bảo không). + +### K3.a — Seed `SeedKhkkApprovalWorkflowsAsync` (khuôn `DbInitializer.cs:171-217`) + +- **Idempotent PER-CODE** (KHÔNG per-type như khuôn `:174-176` — vì 8 code cùng type): `if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;` +- **Fail-soft** thiếu user: `FindByEmailAsync` từng email, null → `LogWarning` + return KHÔNG seed (mirror `:179-183`), boot không chết. +- 8 workflow: `Code = "KHKK-N1".."KHKK-N8"`, `ApplicableType = 10 (ContractSigningPlan)`, `Version=1`, `IsActive=true`, **`IsUserSelectable=true`** (picker lọc isUserSelectable — `KhkkCreatePage.tsx:56-66`), `ActivatedAt=UtcNow`. +- Tên VN (từ transcribe): N1 "KHKK — Nhóm 1: Thiết bị, máy móc, vật tư phụ (A1)" · N2 "…Nhóm 2: Kết cấu, hạ tầng, phá dỡ + Dịch vụ hỗ trợ thi công (A2+A3)" · N3 "…Nhóm 3: Hoạt động văn phòng (A4)" · N4 "…Nhóm 4: Vật tư (B1)" · N5 "…Nhóm 5: Thầu phụ xây dựng (B2)" · N6 "…Nhóm 6: Thầu phụ MEP (B3)" · N7 "…Nhóm 7: BCH công trường (C1+C2+C3)" · N8 "…Nhóm 8: CEO — HĐ chưa ủy quyền (5.2)". +- **Đội hình GIỐNG NHAU cả 8** (OG-2 "đội hình cũ"; ký-nháy KHÁC nhau nằm ở TEXT catalog K1 — QĐ-4 2-tầng, KHÔNG nằm ở Levels): + Step 1 "Phòng Cung ứng" (Dept Code `PRO`) — C1 Order=1 OR-of-4 · C2 Order=2 ×1 + Step 2 "Phòng KSCP" (Dept `CCM`) — C1 Order=1 OR-of-4 · C2 Order=2 ×1 **`AllowApproverFinalize=true`** (QĐ-9: a Chương quyết kết-thúc-hay-lên-CEO) + Step 3 "Ban Giám đốc" (Dept `BOD`) — C1 Order=1 ×1 +- **Bảng resolve email (đối chiếu `DbInitializer.cs:1613-1632` — 14 user Solutions thật):** + | Vai | Tên roster | Email seed | Nguồn | + |---|---|---|---| + | PRO C1 | Phượng | `phuong.nguyen@solutions.com.vn` (Nguyễn Thị Bích Phượng) | `:1614` | + | PRO C1 | Duy | `duy.nguyen@solutions.com.vn` (Nguyễn Văn Duy) | `:1617` | + | PRO C1 | Long | `long.chau@solutions.com.vn` (Châu Tạ Kim Long — ⚠️ ĐỪNG nhầm `long.nguyen@` HRA/ISO `:1629`) | `:1616` | + | PRO C1 | Thanh | `thanh.lethanh@solutions.com.vn` (Lê Thanh Bình) | `:1615` | + | PRO C2 | Trà | `tra.bui@solutions.com.vn` (Bùi Lê Thủy Trà, TP Cung ứng) | `:1613` | + | CCM C1 | Nguyên | `nguyen.ho@solutions.com.vn` (Hồ Thị Nữ Nguyên) | `:1623` | + | CCM C1 | Lưu | `luu.tran@solutions.com.vn` (Trần Xuân Lưu) | `:1622` | + | CCM C1 | Trưởng ⚠️ | `truong.le@solutions.com.vn` (Lê Trần Đăng Trường, NV CCM) — ⚠️ roster ghi "Trưởng", DB có "Trường"(Lê `:1626`) + "Trình"(`tring.le@` `:1625`): xác nhận qua verify-LIVE QT-DN-V2-001, lệch thì hỏi owner | `:1626` | + | CCM C1 | Binh | `binh.le@solutions.com.vn` (Lê Văn Bình) | `:1621` | + | CCM C2 | Chương | `chuong.phan@solutions.com.vn` (Phan Văn Chương, TP KSCP) | `:1620` | + | BGĐ | Trường | `truong.nguyen@solutions.com.vn` (Nguyễn Văn Trường, CEO) | `:1632` | + +### K3.b — Validator phiếu-pin-đúng-nhóm (map Code↔ApprovalGroup = CONVENTION parse suffix) + +- **QUYẾT: convention `^KHKK-N([1-8])$` — 0 mig, 0 cột mới.** Mở rộng `ContractSigningPlanScope.EnsureWorkflowTypeAsync` (`Features.cs:350` create + `:458-462` re-pin UpdateDraft — 2 call-site cùng helper = sửa 1 chỗ) thành `EnsureWorkflowTypeAndGroupAsync(db, awId, approvalGroup, ct)`: nếu `workflow.Code` match pattern → group parse PHẢI == plan.ApprovalGroup, lệch → `ConflictException("Quy trình KHKK-N{x} không thuộc nhóm duyệt N{y} của phiếu.")` (DoD: phiếu nhóm 3 pin nhóm 5 → 409); Code KHÔNG match pattern (vd `QT-KHKK-V2-001` — DEFAULT_CODE_BY_TYPE `ApprovalWorkflowsV2Page.tsx:170` gợi ý admin tự tạo) → **pass + LogWarning** (không chặn phiếu UAT cũ/quy trình tay). Phương án cột `ApprovalWorkflows.ApprovalGroup` tường minh hơn = defer, chỉ nâng khi owner cần chặn cứng quy trình tay. + +### K3.c — 8 menu-con designer + resolvePath + route per-code (fe-admin ONLY — designer không có ở fe-user, S155) + +- MenuKeys.cs +8 const `AwV2_KhkkN1..AwV2_KhkkN8` — **NGOÀI `All`** (0 policy mới, tiền lệ `HopDongCung`/leaf `Pe_*`); +8 MenuItem row con node `ApprovalWorkflowsV2` (`DbInitializer.cs:1801` vùng seed menu — QĐ-7 CHỪA subtree này khi thu gọn admin), Label "Nhóm n — ", Order n. +- fe-admin `Layout.tsx` resolvePath vùng `:166-172` (map `AwV2_*` 3-code hiện tại) **+8 entry** `AwV2_KhkkN{n}` → `/system/approval-workflows-v2/ContractSigningPlan?code=KHKK-N{n}` — leaf lạ KHÔNG map = rớt im lặng #50, phải đủ 8. +- `ApprovalWorkflowsV2Page.tsx`: deep-link type `ContractSigningPlan` ĐÃ MỞ (`:155-163` TYPE_CODE_TO_INT slot 10 `:147`); THÊM đọc `?code=` → filter danh sách definition đúng Code + prefill ô "Mã quy trình" = code đó (thay gợi ý `:164-171`). Mở designer nhóm n load đúng `KHKK-N{n}` (DoD l3-F10). +- mirror `fe-admin/src/lib/menuKeys.ts` +8 const. + +### K3.d — Test (k=5) + +1. `SeedKhkk_8Workflows_Idempotent` — chạy ×2 → đúng 8 row type-10, mỗi Code 1 (đếm-8). +2. `SeedKhkk_Shape_PerWorkflow` — 3 Steps; PRO Order1×4+Order2×1; CCM Order1×4+Order2×1 có `AllowApproverFinalize=true` đúng level Chương; BOD ×1. +3. `SeedKhkk_MissingUser_SkipWarn_NoBootCrash` — thiếu 1/11 email → skip + warn. +4. `CreateOrRepin_WrongGroupCode_409_CustomCode_PassWarn` — K3.b 3 chiều (đúng/lệch/code-lạ). +5. `KhkkEarlyFinalize_ChuongTick_DaDuyet_FreezeApplied_RuntimeFlagSet` — nhánh (a) K3.⚠️: qua choke-point, `ApprovedAmount` không null, `EndedByLevelFinalize=1`, marker Changelog ghi. + +### K3.e — Checklist acceptance K3 + +- [ ] Verify-LIVE đội hình: curl prod `GET /api/approval-workflows-v2` (hoặc Dev + flag) — đối chiếu 11 email bảng K3.a TRƯỚC khi commit seed; ô ⚠️ "Trưởng" chốt được thì ghi vào PR, không thì hỏi owner. +- [ ] Restart API ×2 → `sqlcmd … "SELECT Code FROM ApprovalWorkflows WHERE ApplicableType=10 AND Code LIKE 'KHKK-N%'"` → **đúng 8**, không nhân. +- [ ] fe-admin sidebar: node ApprovalWorkflowsV2 đếm được **8 menu-con**; click "Nhóm 3" → designer load đúng `KHKK-N3`. +- [ ] curl create phiếu group=3 pin `KHKK-N5` → **409**; pin `KHKK-N3` → 201; duyệt trọn trạm 1 phiếu → DaDuyet (DoD §③-K3). +- [ ] Chương tick kết-thúc-tại-cấp → phiếu DaDuyet + mọi line có ApprovedAmount + cờ runtime =1; ô Designer duyệt-sớm type-10 hết khoá (note `:151-152` gỡ). +- [ ] `dotnet test` +5, khai k=5. ## Thứ tự commit + phụ thuộc chéo -(ĐANG LÀM) + +**Tuyến tính K1 → K2 → K3; seed-86 tách chờ OG-6 (không chặn nhánh sau).** + +1. **Commit K1-code** `[CLAUDE] Domain+Infra+App: add ContractCatalogEntry + Mig 70 + CQRS + FE kind` — entity/config/DbSet ×2 interface + **Mig 70** 3-file + `ContractCatalogFeatures.cs` + extend `CatalogsController` + FE ×2 KIND_CONFIG + menu row + menuKeys mirror + test K1 {3,4,5}. Restart local verify bảng rỗng OK. ⚠️ update `SolutionErp.slnx` KHÔNG cần (không .csproj mới) — chỉ file .cs mới trong project sẵn. +2. **Commit K1-seed** (SAU [OG-6] anh gật transcribe): `SeedContractCatalogAsync` 86-row + test K1 {1,2}. Restart ×2 → 86 idempotent. **Prod:** deploy xong seed tự chạy (UNGATED) — không cần tay. +3. **Commit K2** `[CLAUDE] App+Infra: KHKK ApprovalGroup + Line catalog + freeze răng (Mig 71)` — **Mig 71 gồm cả item 6 `EndedByLevelFinalize`** (forward-provision cho K3 — QUYẾT gộp để K3 **0-mig**; nếu lead tách thì K3 tự mang Mig 72) + rào 2 site + validator cùng-nhóm + list param + DTO + FE types + test K2 ×7. **Phụ thuộc K1-code** (validator K2.d query bảng catalog — phải CÓ bảng; không cần data 86). Σ PeReferenceAmount đo TRƯỚC/SAU migrate ghi PR. +4. **Commit K3** `[CLAUDE] Infra+FE-Admin: seed 8 KHKK workflows + early-finalize + designer per-code` — seed 8 + nhánh duyệt-sớm service + validator pin-group + 8 menu-con + Layout +8 + `?code=` + gỡ note khoá `:151-152` + test K3 ×5. **Phụ thuộc K2** (cột `ApprovalGroup` + `EndedByLevelFinalize`). PRE: verify-LIVE đội hình (K3.e bước 1) TRƯỚC khi commit. +5. Mỗi commit qua Gitea CI (~3 phút, path-filter docs-only 0s); prod IIS restart → `DbInitializer.MigrateAsync` áp mig + seed tự chạy; **restart ×2 verify idempotent** (khuôn S91). Docs cuối cụm: `STATUS.md` (Mig 69→**71**, bảng 96→**97**, test +**17**), `schema-diagram.md` +2 row mig, `HANDOFF.md`, tick `migration-todos.md`. + +**Phụ thuộc chéo tóm tắt:** K2.d cần bảng K1 (không cần 86 row — null-pass) · K3.a seed KHÔNG đọc catalog (label hardcode) — chỉ cần K2 cột · K3.⚠️(b) cột runtime NẰM TRONG Mig 71 = K2 phải land trước K3 · seed-86 lơ lửng độc lập chờ OG-6 · K4a (cụm-2) 0 phụ thuộc cụm-1, chạy song song được (l3-F8). ## Checklist tổng cụm-1 -(ĐANG LÀM) + +- [ ] **Mig:** `__EFMigrationsHistory` top = Mig **71**; Mig 70+71 mỗi cái đủ **3 file** (`git show --stat`); bảng `sys.tables` = **97**. +- [ ] **K1:** catalog active = **86**; phân bố nhóm `GROUP BY ApprovalGroup` = **5/10/5/23/14/15/13/1**; restart ×2 không nhân; CRUD 200/403 đúng vai; tên VN+EN 86/86. +- [ ] **K2:** `COUNT(ContractSigningPlans)` == Σ COUNT 8 nhóm (backfill=1 đủ); phiếu nhóm-2-cùng-PE → **201**, trùng nhóm → **409**; line khác nhóm → 409; `SUM(PeReferenceAmount)` TRƯỚC == SAU (2 số ghi PR); grep `Phase\s*=[^=]` liệt đủ site PR; bảng 17-consumer dán PR TỪNG DÒNG. +- [ ] **K3:** đúng **8** workflow `KHKK-N%` type-10; sidebar admin **8 menu-con**, designer nhóm-n load `KHKK-N{n}`; pin lệch nhóm → 409; duyệt trọn trạm → DaDuyet; Chương tick kết-thúc → DaDuyet + freeze + cờ runtime; 2 ô Designer type-10 hết khoá. +- [ ] **Test:** `dotnet test SolutionErp.slnx` = baseline (STATUS row Tests) + **17** (k khai: K1=5 · K2=7 · K3=5), 0 fail. +- [ ] **CI/prod:** 4 commit CI PASS; BE-only wave = smoke API (l3-F17, KHÔNG cần byte-verify #77 trừ commit có FE-ship → curl-grep bundle marker); STATUS/schema-diagram/HANDOFF cập nhật. +- [ ] **OG còn mở khi land:** OG-6 (chặn DUY NHẤT commit K1-seed) · ô ⚠️ "Trưởng" K3.a (chốt qua verify-LIVE hoặc hỏi owner). + +END sub-invest-fable-b2-cum1 — TOTAL: K1 6v · K2 8v · K3 6v · 17 test. diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md new file mode 100644 index 0000000..58f0545 --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md @@ -0,0 +1,12 @@ +# c1-l1 QUYẾT-vs-code + +Lane 1/3 review cụm-1 — lens: QUYẾT (spec chốt) đối chiếu CODE trên đĩa. +Đối tượng: `.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md` + +## Findings + +(ĐANG LÀM) + +## Verdict + +(ĐANG LÀM) diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md new file mode 100644 index 0000000..d95a6cf --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md @@ -0,0 +1,12 @@ +# c1-l2 đủ-vs-spec + +Lane 2/3 — review cụm-1 (`sub-invest-fable-b2-cum1.md`) đối chiếu spec-v2 (`spec-4gd-khkk-tong-quat-31-07-2026.md`). +Lens: **ĐỦ-vs-SPEC** (nghĩa vụ nào của spec-v2 KHÔNG được cụm-1 gánh). + +## Findings + +(ĐANG LÀM) + +## Verdict + +(ĐANG LÀM) diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md new file mode 100644 index 0000000..e73c41a --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md @@ -0,0 +1,13 @@ +# c1-l3 đo-được + thứ-tự + +> LANE 3/3 review cụm-1 (spec B2-r). Lens: **ĐO-ĐƯỢC** (test-case có assert? checklist có lệnh+số?) + **THỨ-TỰ** (commit/mig/restart/deploy/seed/phụ-thuộc-chéo). +> Target chính: `sub-invest-fable-b2-cum1.md` (33.048B) · liếc `spec-4gd-khkk-tong-quat-31-07-2026.md` §③. +> ⚠️ Path lệch trong spec: TASK yêu cầu `sub-reviewer-c1-l3.md`, footer RUN-TRACE ghi `sub-reviewer-2.md` → theo TASK (fix-mới thắng footer-cũ, S139); khai trong report. + +## Findings + +(ĐANG LÀM) + +## Verdict + +(ĐANG LÀM)