# SPEC WAVE-1 — Schema Mig 69 + Permission + Designer type-10 (GĐ2 KHKK)

> Nguồn RATIFIED: spec cũ §2.2 (7 bảng) + §2.4 (menu/permission) + §③ WAVE-1 (`spec-ke-hoach-ky-ket-hd-28-07-2026.md:266-308`) + NOTE W1 @S160 (đầu §WAVE-1). Plan cha default #6 #12. File này = CÁCH-LÀM cụ thể + checklist; xung đột nào với spec cũ đã khai ở mục Δ.

## ① TÍNH NĂNG / ĐỀ BÀI

Đặt nền DB + quyền + cho admin dựng được quy trình duyệt `type=10`, để W2 tạo phiếu được ngay. Deploy riêng an toàn: menu skeleton `Khkk_*` đã public từ arc-2 (leaf ComingSoon — dark-launch nằm ở TRANG, không ở menu). **Mig 69 = migration DUY NHẤT của toàn plan dry-run.**

**Δ so spec cũ (2 điểm, do thực địa arc-2 đổi tiền đề):**
1. **KHÔNG thêm 4 const `ContragSigningPlans*`/menu 4 dòng** như §2.4 — menu skeleton `Khkk_G1`+6 leaf ĐÃ SEED prod (CanRead 13/13). Thay bằng: `All += KeHoachKyKet` (const sẵn `MenuKeys.cs:41`) → policy `KeHoachKyKet.{Read,Create,Update,Delete}` tự sinh (`Program.cs` foreach All×Actions) + seeder grant 4-action cho role đích. Menu-key hiển thị (`Khkk_*`) ⟂ policy-key (`KeHoachKyKet`) — 2 lớp khác nhau, khai rõ để reviewer soi.
2. Label VN = **"Kế hoạch ký kết HĐ"** (đời nhãn cuối @S159), KHÔNG phải "Đề xuất ký kết hợp đồng" còn trong spec cũ `:223`.

## ② CÁCH IMPLEMENT (thứ tự bắt buộc)

1. **Gate 5-anchor** (§③-A) — trượt ≥1 ⇒ chấm lại hết rồi mới code.
2. **Domain** (`src/Backend/SolutionErp.Domain/ContractSigningPlans/`): 7 entity đúng bảng spec cũ §2.2 (#1 header `ContractSigningPlans` [+`DepartmentId Guid?` C5] · #2 `Lines` [+`ContractId Guid?` C6, UNIQUE filtered `(PlanId,SupplierId) WHERE IsDeleted=0`] · #3 `DossierItems` · #4 `LevelOpinions` [UNIQUE (PlanId,LevelId), FK Cascade-Plan/Restrict-Level] · #5 `Attachments` · #6 `Changelogs` [ĐỦ 4 cột EntityType/EntityId/UserName/FieldChangesJson] · #7 `Approvals`). 2 enum: `ContractSigningPlanPhase{DangSoanThao=1,ChoDuyet=2,DaDuyet=3,TraLai=98,TuChoi=99}` · `DossierItemStatus{ChuaNop=1,DaNop=2,TvgsDuyet=3,TvgsBac=4}` (🔴 bắt đầu 1, KHÔNG 0 — falsy JS) + `DossierItemKind{MauVatLieu=1,Shopdrawing=2,Khac=99}`.
3. **`ApprovalWorkflowApplicableType` += `ContractSigningPlan = 10`** — re-verify slot 10 trống NGAY TRƯỚC khi viết (`ApprovalWorkflow.cs:53-67` dừng ở 9; anchor #2).
4. **EF Configuration ×7** — khai độ dài MỌI cột chuỗi (spec cũ `:134-137`: MaKeHoach 50 · GhiChu/ContextNote 2000 · Name 500 · Note 1000 · TvgsName 200 · Summary 1000 · SignedByFullName 200 IsRequired · FileName 500/Path 1000/ContentType 200) + `HasQueryFilter(!IsDeleted)` header + loose-Guid (KHÔNG FK vật lý sang PE/Supplier — convention Mig 49).
5. **DbSets** vào `IApplicationDbContext` + `ApplicationDbContext`.
6. **Mig 69** `AddContractSigningPlans` (3-file rule: migration + Designer + Snapshot; skill `ef-core-migration`): 7 CreateTable, 0 ALTER bảng cũ, Down = DropTable ×7. `database-agent` design-review TRƯỚC apply (per spec cũ W1 vai).
7. **MenuKeys.cs**: `All` += `KeHoachKyKet` (chỉ 1 dòng — const đã có :41). KHÔNG key mới.
8. 🔴 **DbInitializer grant — UPGRADE-IF-EXISTS, KHÔNG INSERT (review F-S1/F-O3/F-O4 — 4 lane hội tụ, lead verify đĩa CONFIRMED):** row Permission key `KeHoachKyKet` + `Khkk_*` **ĐÃ tồn tại CanRead-only cho mọi role** (`DbInitializer.cs:2204-2213` nhánh read-only), và nhánh grant key-non-Pe là **skip-existing `continue`** (`:2242-2245`) ⇒ thêm grant kiểu **insert = NO-OP im lặng** ⇒ CanCreate/Update giữ `false` ⇒ **W2/W3 lên prod 403 cho 13/13 role, chỉ Admin qua = GIẾT dry-run** (gotcha #44). **Fix:** viết method mới khuôn **`SeedProcurementMasterAccessAsync:2367-2374`** (`if (existing.TryGetValue(key,out row)) { if(!row.CanCreate){row.CanCreate=true;…} }`), chạy **SAU** `RevokeTemporarilyHidden…` (mirror `:2098-2101`) để thắng. Cấp CanCreate/CanUpdate cho **13/13 role** (đúng O-A "hiển thị hết để góp ý") + CanDelete cho Drafter/Admin, trên `KeHoachKyKet` + 6 leaf `Khkk_*`. 🔴 **CẤM SQL tay** (gotcha #84 — vết `DbInitializer.cs:2287-2296` Run #423 grant tay bị revoker lật 447/494). (b) KHÔNG đụng menu-row `Khkk_*` (đã seed). (c) label chỉnh = labelBackfill (gotcha #11).
9. **Mã phiếu**: CodeGen service prefix `"KHKK/{YYYY}"` qua `WorkflowAppCodeSequence` (spec cũ #8-CẮT: bảng dùng chung 4 module Office, atomic = transaction SERIALIZABLE trong service, `ContractCodeSequence.cs:5`). ✅ **Format `KHKK/{YYYY}/{Seq:D3}` anh OK @S161** (default #6 RESOLVED).
10. **fe-admin Designer type-10**: `ApprovalWorkflowsV2Page.tsx` map `TYPE_CODE_TO_INT` (site thực `:144-148`, KHÔNG phải comment `:143` — review chéo) + 3 site `ApprovalWorkflowV2AdminFeatures.cs` liệt-kê-thủ-công (`:911/:929` merge chữ-ký-sống · `:971/:992` RetainedOpinionRef · `:1044/:1056` chữ ký mồ côi) — thiếu ⇒ admin xoá Level có chữ ký nổ FK 500.
    🔴 **Ẩn 2 checkbox early-finalize cho type=10 (review F-B3 — containment THẬT):** Designer V2 render checkbox `AllowApproverFinalize`/`AllowApproverSkipToFinal` (`ApprovalWorkflowsV2Page.tsx:1256-1257` SkipToFinal + `:1267-1268` Finalize) = per-Level admin opt-in. Anh phán 2 đường này **"PHÁ VỠ"** (S155). W3 KHÔNG port code path ⇒ để checkbox sống mà backend không xử = cờ chết đánh lừa admin. **Fix:** ẩn/disable 2 checkbox khi `typeCode==10` (hoặc render read-only + tooltip "chưa hỗ trợ cho quy trình KHKK"). Đây là cách làm default #1 ("đường THƯỜNG") thành **containment thật** thay vì "port-nhưng-không-kích-hoạt" (giả).
11. Build + test + commit `[CLAUDE] Domain+Infra+Admin: W1 KHKK schema Mig 69 + policy KeHoachKyKet + Designer type-10`.

## ③ CHECKLIST

**A. Gate 5-anchor (mở TRƯỚC):** `ApprovalWorkflow.cs:53-67` (slot 10 trống) · `MenuKeys.cs:41` (const KeHoachKyKet + comment tái dùng) · `MenuKeys.cs:163+` (All chưa có KeHoachKyKet) · `AuthorizePolicyRegressionTests.cs:138-139` (lưới policy-ngoài-All ⇒ 500) · `DbInitializer.cs:1779-1786` (Khkk_* 7 key đã seed).
**B. Acceptance (từ spec cũ W1 `:281-300`, giữ nguyên hiệu lực + bổ sung):**
- [ ] `dotnet ef migrations list` → migration cuối chứa `AddContractSigningPlans` (KHÔNG có tiền tố `69_`).
- [ ] `sqlcmd` local: `SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped=0` → **89 → 96**.
- [ ] `dotnet test` → `AuthorizePolicyRegressionTests` XANH (policy `KeHoachKyKet.*` hợp lệ — All đã chứa key).
- [ ] Test mới tồn tại ĐÚNG TÊN: `ContractSigningPlanSchemaTests.BayBang_MoiBang_CoAuditFields_VaQueryFilter` + sàn `N ≥ 2` test mới, PASS 0 fail (số-sau ≥ số-đo-ngay-trước + 2).
- [ ] Phép đo mã phiếu (review A6 — quyết owner-visible phải có acceptance): test `SigningPlanCodeGenerator_Format_KhkkYearSeqD3` — mã match `^KHKK/\d{4}/\d{3}$` + 2 lần gọi liên tiếp seq tăng dần (format anh OK @S161).
- [ ] Admin tạo được quy trình `type=10` 3 Bước (PMH→CCM→CEO), pin `IsUserSelectable`; xoá 1 Level chưa chữ ký → OK.
- [ ] `dotnet ef migrations script` + **Down chạy sạch trên DB copy**.
- [ ] 🔴 Policy-grant ĐO CỜ, KHÔNG đếm ROW (review F-04: row đã có sẵn ⇒ đếm row = 0-bit): sau seed, `sqlcmd`/curl xác nhận Permission key `KeHoachKyKet` có **CanCreate=1 ∧ CanUpdate=1** cho ≥1 role non-Admin (vd Drafter). Row đã tồn tại từ trước ⇒ chỉ CỜ mới chứng upgrade ăn.
- [ ] cicd 3-chân-kiềng sau deploy: CI PASS · `__EFMigrationsHistory` top = Mig 69 (curl API không đo được mig ⇒ dùng sqlcmd qua RDP-người hoặc chấp nhận CI-log làm chứng) · sys.tables 96 · menu KHÔNG đổi (142 giữ — W1 không thêm menu).
**C. Rủi ro tick trước khi đóng wave:** [ ] không quên HasQueryFilter · [ ] UNIQUE filtered có WHERE IsDeleted=0 (gotcha #57) · [ ] 0 SQL tay (gotcha #84).
