# impl-be K1

## Tiến độ
- [x] B1 Đọc spec 4 nguồn: `sub-invest-fable-b2-cum1.md` §K1 (:9-75) · `spec-cum1-chi-tiet-31-07-2026.md` §② vá-1 (:8) · `danh-muc-sp002-transcribe.md` (86 row) · `spec-4gd-khkk-tong-quat` §③ K1 DoD (:24)
- [x] B2 Soi khuôn: `Domain/Master/Catalogs/WorkItem.cs` (entity) · `Configurations/CatalogsConfiguration.cs` (filtered-unique #57) · `Api/Controllers/CatalogsController.cs:12-14` (authz mẫu) · `Application/Master/Catalogs/CatalogsFeatures.cs` (CQRS mẫu) · `DbInitializer.cs:2901` (seed khuôn) / `:1766-1769` (menu leaf) / `:2058` (grant Admin loop)
- [x] B3 Entity `ContractCatalogEntry` + Config + DbSet ×2 (ApplicationDbContext + IApplicationDbContext)
- [ ] B4 Mig 70
- [ ] B5 Seeder 86 row
- [ ] B6 MenuKeys + MenuItem row
- [ ] B7 CQRS + controller
- [ ] B8 build + test

## Lệch spec (nếu có)
1. **MenuKeys VÀO `All`** (lead) vs **NGOÀI `All`** (invest-spec K1.d:57) → theo lead, khai drift bên dưới.
2. **Controller riêng** `ContractCatalogController` (lead) vs **extend `CatalogsController`** (invest-spec K1.c:51) → theo lead về FILE, giữ PATH `api/catalogs/contract-catalog` của spec để FE khỏi đổi.
3. **TenVi B2-07..B2-14 bỏ chú-thích `(ảnh: …)`** — chú-thích của người chép, không phải tên hạng mục; chuyển thành comment cuối dòng. Row vẫn đủ 14.
4. **Test K1 k=5 (invest-spec K1.e) KHÔNG viết** — lead chỉ định "KHÔNG viết test mới, chỉ KHÔNG VỠ"; test là việc `test-specialist`.

## Kết quả build/test
- `dotnet build SolutionErp.slnx --nologo -v minimal` → **Build succeeded · 0 Warning(s) · 0 Error(s)** · Time Elapsed 00:00:15.33
- `dotnet test SolutionErp.slnx --nologo -v minimal` →
  - `Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45` (Domain, 332 ms)
  - `Passed! - Failed: 0, Passed: 545, Skipped: 0, Total: 545` (Infrastructure, 1 m 25 s)
  - **Tổng 590 PASS / 0 FAIL** = ĐÚNG baseline 590 (`STATUS.md:472`) → 0 test vỡ, 0 test mới (đúng lệnh).

## FILE tạo/sửa (PHẦN 2 — lượt này)
**MỚI (2):**
- `src/Backend/SolutionErp.Application/Master/Catalogs/ContractCatalogFeatures.cs` — 5 handler + 2 validator + DTO
- `src/Backend/SolutionErp.Api/Controllers/ContractCatalogController.cs` — 5 endpoint, `[Authorize]` + 3× `[Authorize(Roles="Admin")]`

**SỬA (2):**
- `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` **+186 dòng** — `SeedContractCatalogAsync` (86 row) + call-site `InitializeAsync` + 1 menu row leaf
- `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs` **+5 dòng** — const `ContractCatalog` + vào `All`

**CÓ SẴN từ lượt 1 (KHÔNG đụng, liệt để lead commit đủ):** `Domain/Master/Catalogs/ContractCatalogEntry.cs` · `Persistence/Configurations/ContractCatalogEntryConfiguration.cs` · Mig 3-file `20260731072441_AddContractCatalogEntries.cs` + `.Designer.cs` + `ApplicationDbContextModelSnapshot.cs` · `IApplicationDbContext.cs` (+2) · `ApplicationDbContext.cs` (+2).
→ **Tổng scope K1-BE = 10 file** (4 lượt-này + 6 lượt-1). `git status -- src/Backend` xác nhận **0 file FE, 0 file tests/** bị chạm.

## Drift DELTA (số canonical `docs/STATUS.md` — em main vá doc, tôi KHÔNG đụng)
| Row | Trước | Sau | Cách đo |
|---|---|---|---|
| Menu keys | 55 | **56** | đếm TỪ MÃ `MenuKeys.All` (sed vùng array → tách `,` → đếm identifier) = **56** |
| Policies | 220 | **224** | DERIVED `\|All\| × \|Actions\| = 56 × 4` |
| Mig | 69 | **70** | `20260731072441_AddContractCatalogEntries` (lượt 1) |
| Bảng | 96 | **97** | +1 `ContractCatalogEntries` (lượt 1) |
| Tests | 590 | **590** | không đổi (0 test mới, 0 vỡ) |

## 🔴 Bàn giao / còn hở (lead quyết)
1. **Mig 70 CHƯA áp Dev-DB** — `sqlcmd (localdb)\MSSQLLocalDB -d SolutionErp_Dev` → top `__EFMigrationsHistory` = `20260729122015_AddContractSigningPlans` (Mig 69). Tôi KHÔNG tự áp (ngoài 7 bước lệnh + lệnh cấm sửa schema). Tự lành khi chạy API: `DbInitializer:64 MigrateAsync()` áp Mig 70 rồi seed 86 row ngay cùng lần boot. Hoặc tay: `dotnet ef database update --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api` (+ `--connection "…Database=SolutionErp_Dev…"` cho Dev).
2. **Chưa chạy verify RUNTIME** (restart API ×2 → COUNT=86 idempotent, K1.f:70-71) vì chưa áp DB — checklist đó còn TRỐNG, đừng tick.
3. **Leaf menu hiện chỉ Admin thấy** — 3 seeder grant khác dùng list-key-cứng (chi tiết P2-B2).
4. **[OG-6] chưa gật** — data 86 row đã land trong code; nếu owner soát ra sai ô nào thì sửa tuple + restart (per-Code idempotent nên row đã seed KHÔNG tự cập nhật, cần UPDATE tay hoặc thêm nhánh upgrade-if-exists).
5. Test K1 k=5 để `test-specialist`.

END impl-be-k1 — 4 file lượt này (10 file scope K1-BE) · build PASS 0W/0E · test 590 (45D+545I) 0 fail

## PHẦN 2 (relaunch)

Lượt 1 chết sau schema-layer. Schema ĐÃ TRÊN ĐĨA (verify @relaunch):
- `src/Backend/SolutionErp.Domain/Master/Catalogs/ContractCatalogEntry.cs` (10 cột, AuditableEntity)
- Mig `20260731072441_AddContractCatalogEntries` (3-file) — KHÔNG tạo lại, KHÔNG sửa.

Việc PHẦN 2: seeder 86-row · MenuKeys + MenuItem · CQRS/controller · build · test.

### P2-B0 Đọc nguồn (xong)
- [x] `sub-invest-fable-b2-cum1.md` §K1.b/.c (:33-51) — seed UNGATED + IgnoreQueryFilters + mapping field; CQRS mega-file + extend CatalogsController
- [x] `danh-muc-sp002-transcribe.md` TRỌN — 86 row (A1 5 · A2 1 · A3 9 · A4 5 · B1 23 · B2 14 · B3 15 · C1 6 · C2 3 · C3 4 · CEO 1 = 86); bảng 8 nhóm (SignerRole + InitialsChain)
- [x] `DbInitializer.cs:2889-2986` khuôn tuple-array + per-Code idempotent
- [x] `MenuKeys.cs:164-186` vùng `All` (5 leaf catalog :168)

### ⚠️ LỆCH SPEC đã khai (lead override, làm theo lead)
- invest-spec §K1.d (:57) ghi `MenuKeys.ContractCatalog` **NGOÀI `All`** (0 policy mới).
- LỆNH lead PHẦN 2 mục 2 ghi **VÀO `All`** + drift DELTA **(+1 key / +4 policy)**.
→ Làm theo LỆNH lead (mới hơn + có khai drift). Hệ quả: `docs/STATUS.md` row `Menu keys` +1 và row `Policies` +4 (DERIVED |All|×|Actions|) — em main cập nhật STATUS, tôi KHÔNG đụng doc.

### P2-B1 Seeder 86 row — XONG
- `DbInitializer.cs` +`SeedContractCatalogAsync` (đặt ngay TRƯỚC `SeedContractTemplatesAsync`) + call-site trong `InitializeAsync` ngay SAU `SeedRealMasterDataAsync` (khớp K1.b "cạnh SeedRealMasterDataAsync, trước sample workflows").
- **UNGATED** (ngoài `if (!demoSeedDisabled)`) — gotcha #51.
- Idempotent per-Code + **`IgnoreQueryFilters()`** khi dò existing → né #75/#76 resurrect (làm ĐÚNG HƠN khuôn `:2970`, ghi comment lý do trong code).
- `NduqNote = null` [OG-3] · `IsActive = true` · `SortOrder` = STT trong section.
- SignerRole/InitialsChain: lookup `signByGroup` 8 nhóm → **ghi denorm vào TỪNG row** (đủ 8 field/row như lệnh; không chép tay chuỗi 86 lần để khỏi lệch ký tự). Ô "N/A" đã bỏ đúng spec (N3 còn 3 ô, N4/N8 còn 2 ô, N7 còn 3 ô).
- **ĐO tại chỗ (grep count trên file vừa ghi):** tổng **86** row; theo section `A1=5 A2=1 A3=9 A4=5 B1=23 B2=14 B3=15 C1=6 C2=3 C3=4 CEO=1`; theo `ApprovalGroup` = **5/10/5/23/14/15/13/1** → khớp ĐÚNG K1.b:41.
### P2-B2 MenuKeys + MenuItem — XONG
- `MenuKeys.cs` +const `ContractCatalog = "ContractCatalog"` (cạnh 4 const catalog, `:28-32`) + **VÀO `All`** ngay sau dòng 5 leaf catalog.
- `DbInitializer.cs` `SeedMenuTreeAsync` +row `(MenuKeys.ContractCatalog, "Danh mục Hợp đồng", MenuKeys.Catalogs, 5, "FileSpreadsheet")` — leaf thứ 5 dưới "Danh mục chi tiết"; icon `FileSpreadsheet` đã dùng sẵn (`MenuKeys.Forms` :1788) nên FE có map.
- Grant Admin: TỰ ĐỘNG qua loop `SeedAdminPermissionsAsync` `MenuKeys.All` (`:2058`) — 0 code thêm.
- ⚠️ **Flag cho lead (KHÔNG tự làm — ngoài lệnh):** 3 seeder grant khác dùng DANH SÁCH KEY CỨNG chứ không prefix-match → key mới KHÔNG tự vào: `SeedCatalogManagerPermissionsAsync` (`:2626-2629`), `SeedAllRolesReviewReadPermissionsAsync` (`:2181-2182`), `SeedProcurementMasterAccessAsync` (`:2360-2363`). ⇒ **hiện chỉ Admin thấy leaf "Danh mục Hợp đồng"**; muốn PRO/CatalogManager thấy thì thêm key vào 3 list đó (mỗi chỗ 1 dòng).

### P2-B3+B4 CQRS + Controller — XONG
- MỚI `SolutionErp.Application/Master/Catalogs/ContractCatalogFeatures.cs`: `ListContractCatalogQuery(GroupCode?, ApprovalGroup?, IsActive?, Q?)` (sort GroupCode→SortOrder→Code, KHÔNG paging) · `GetContractCatalogEntryQuery` · `Create/Update/Delete` + 2 validator.
- Validator MaxLength **lấy từ EF config** `ContractCatalogEntryConfiguration.cs:18-24` (Code 20 · TenVi/TenEn/NduqNote 500 · GroupCode 10 · SignerRole 100 · InitialsChain 300) — không trust spec; `ApprovalGroup` `InclusiveBetween(1,8)`.
- Dup-Code: `AnyAsync(x => x.Code == req.Code)` chạy trên ACTIVE-set (query filter `!IsDeleted`) → khớp UNIQUE filtered `[IsDeleted]=0`, Code của row xoá-mềm tái dùng được. Delete = `Remove` → soft qua AuditingInterceptor.
- MỚI `SolutionErp.Api/Controllers/ContractCatalogController.cs` — `[Route("api/catalogs/contract-catalog")]` + class `[Authorize]`, 3 verb ghi `[Authorize(Roles = "Admin")]`. **0 policy key mới**.
  - Ghi chú hòa 2 lệnh: lead nói "controller ContractCatalogController", invest-spec K1.c:51 nói "extend CatalogsController" với path `api/catalogs/contract-catalog` → tôi lấy **file controller riêng (theo lead)** + **giữ nguyên path (theo spec)** ⇒ FE KIND_CONFIG `contract-catalog` chạy được không cần đổi.

### P2-B5 Build — PASS
`dotnet build SolutionErp.slnx --nologo -v minimal` → **Build succeeded · 0 Warning · 0 Error** (15.3s, 6 project) — PASS ngay lần đầu, 0 vòng sửa compile.

- ⚠️ **1 quyết-định data khai rõ:** transcribe B2-07..B2-14 có chú-thích truy-vết `(ảnh: số 7 thứ nhất)`, `(ảnh: 9)`… — đó là **ghi-chú của người chép, KHÔNG phải tên hạng mục**. Tôi seed TenVi **sạch** ("Công tác kim loại", "Cảnh quan"…) và chuyển chú-thích thành comment cuối dòng code. Số dòng giữ nguyên 14 (AS-IS về row). Nếu owner muốn giữ nguyên văn trong tên → sửa 8 chuỗi, 1 phút.

