# sub-implementer-backend-0 — W1 KHKK lane BE (RELAUNCH-CẮT-GỌT, tiếp lane chết #53)

> Ghi TỪNG MỤC ngay sau khi xong mục đó (lane trước chết trước khi kịp ghi).
> Run: `2026-07-29-S161-khkk-w1-schema` · spec `spec-wave-w1-schema-designer-29-07-2026.md` §② 2→9.

## §0 — AUDIT mục "✅ ĐÃ XONG" (trước khi làm tiếp)

| Vật | Đo | Verdict |
|---|---|---|
| `Domain/ContractSigningPlans/` | 8 file (7 entity + `ContractSigningPlanPhase.cs`) | PASS |
| `ContractSigningPlanPhase` | `:10-14` = DangSoanThao=1 / ChoDuyet=2 / DaDuyet=3 / TraLai=98 / TuChoi=99 | PASS khớp spec `:16` |
| `DossierItemKind` | `ContractSigningPlanDossierItem.cs:7-12` = MauVatLieu=1/Shopdrawing=2/Khac=99 | PASS |
| `DossierItemStatus` | `ContractSigningPlanDossierItem.cs:17-23` = ChuaNop=1..TvgsBac=4 (KHÔNG 0) | PASS — vá C2 |
| `ApprovalWorkflowApplicableType.ContractSigningPlan = 10` | `ApprovalWorkflow.cs:66` (append-only, sau `TravelRequest = 9`) | PASS |
| Header +`DepartmentId Guid?` (C5) | `ContractSigningPlan.cs:21` | PASS |
| Lines +`ContractId Guid?` (C6) | `ContractSigningPlanLine.cs:25` | PASS |
| Changelog ĐỦ 4 cột (C1) | `ContractSigningPlanChangelog.cs:29,30,34,36` EntityType/EntityId/UserName/FieldChangesJson | PASS |
| Configuration đã có | `ContractSigningPlanConfiguration` + `...Line` + `...DossierItem` + `...LevelOpinion` = 4/7 | đúng như brief |
| UNIQUE filtered Lines | `ContractSigningPlanLineConfiguration.cs:22-24` `(PlanId,SupplierId)` + `HasFilter("[IsDeleted] = 0")` | PASS gotcha #57 |
| UNIQUE LevelOpinions | `ContractSigningPlanLevelOpinionConfiguration.cs:36` `(PlanId,LevelId)` IsUnique KHÔNG filter | PASS (parity 7 twin, UPSERT) |

→ 0 lệch spec ⇒ KHÔNG sửa gì phần đã xong.

## MỤC 1 — 3 Configuration còn lại ✅

3 file MỚI (khuôn = 4 config đã có + twin PE `PurchaseEvaluationConfiguration.cs:156-210`):

1. `Configurations/ContractSigningPlanAttachmentConfiguration.cs`
   - FileName 500 IsRequired · StoragePath 1000 IsRequired · ContentType 200 IsRequired (spec `:134-137`;
     RỘNG hơn PE 255/500/100 — khuôn `ProposalAttachmentConfiguration.cs:15-17`) · Note 1000 · Purpose `HasConversion<int>()`
   - FK **Cascade** Plan; `ContractSigningPlanDossierItemId` chỉ `HasIndex` (KHÔNG FK — tránh MULTIPLE CASCADE PATHS)
   - `HasQueryFilter(!IsDeleted)` — AuditableEntity
2. `Configurations/ContractSigningPlanChangelogConfiguration.cs`
   - Summary **1000** (spec, PE để 500) · ContextNote 2000 · UserName 200 · FieldChangesJson `nvarchar(max)`
   - IX `(PlanId, CreatedAt)` + `(PlanId, EntityType)` (index twin C1)
   - BaseEntity ⇒ **KHÔNG** HasQueryFilter (parity PE Changelog)
3. `Configurations/ContractSigningPlanApprovalConfiguration.cs`
   - Comment 1000 · From/ToPhase/Decision `HasConversion<int>()`
   - IX `(PlanId, ApprovedAt)` (twin PE `:168`) + IX `ApprovalWorkflowLevelId`
   - `ApprovalWorkflowLevelId` loose-Guid KHÔNG FK (nếu Restrict → admin xoá Level nổ 500, vì 3 site dọn dẹp
     chỉ biết họ `*LevelOpinions`)
   - BaseEntity ⇒ KHÔNG HasQueryFilter

⚠️ Cả 7 bảng con đều FK Cascade 1-hop từ Plan ⇒ 0 multiple-cascade-path.

## MỤC 2 — DbSets ×7 ✅

- `Application/Common/Interfaces/IApplicationDbContext.cs:3` +`using SolutionErp.Domain.ContractSigningPlans;`
  · `:144-153` block 7 DbSet + chú thích "mã KHKK qua `WorkflowAppCodeSequences` (:136) DÙNG CHUNG, KHÔNG bảng sequence riêng".
- `Infrastructure/Persistence/ApplicationDbContext.cs:5` +using · `:129-136` 7 `public DbSet<...> => Set<...>()`.
- Config tự nạp qua `ApplyConfigurationsFromAssembly` (`ApplicationDbContext.cs` cuối `OnModelCreating`) — 0 dòng đăng ký tay.

## MỤC 4 (làm sớm, trước build) — MenuKeys ✅

`Domain/Identity/MenuKeys.cs:179` — thêm ĐÚNG 1 dòng `KeHoachKyKet,` vào `All` (const sẵn `:41`, KHÔNG key mới).
⇒ `Program.cs` foreach `All × Actions` tự sinh 4 policy `KeHoachKyKet.{Read,Create,Update,Delete}`.

## Build sau mục 1+2+4

`dotnet build SolutionErp.slnx` → **Build succeeded. 0 Warning(s) / 0 Error(s)** (56.77s, 6 project).

## MỤC 3 — Mig 69 `AddContractSigningPlans` ✅

Lệnh: `dotnet ef migrations add AddContractSigningPlans --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api --output-dir Persistence/Migrations`

**3-file rule (gotcha #17) — `git status` chứng:**
- `?? 20260729122015_AddContractSigningPlans.cs` (22.059 B)
- `?? 20260729122015_AddContractSigningPlans.Designer.cs` (264.515 B)
- ` M ApplicationDbContextModelSnapshot.cs`

**Nội dung đo (grep, KHÔNG đọc-mắt):**
- `grep -c CreateTable` = **7** · Down = **7 DropTable** (`:350-368`) · **0** `AddColumn/DropColumn/AlterColumn/Sql/RenameColumn` ⇒ 0 ALTER bảng cũ.
- UNIQUE filtered Lines `:301-306`: `(ContractSigningPlanId, SupplierId)` `unique: true, filter: "[IsDeleted] = 0"` ✅ gotcha #57
- UNIQUE LevelOpinions `:287-291`: `(ContractSigningPlanId, ApprovalWorkflowLevelId)` `unique: true` (KHÔNG filter — đúng parity twin)
- UNIQUE MaKeHoach `:325-330`: filter `[MaKeHoach] IS NOT NULL`
- FK: Plan→ApprovalWorkflows **Restrict** `:47` · LevelOpinions→ApprovalWorkflowLevels **Restrict** `:201` · 6 con→Plan **Cascade** (`:76,108,139,171,207,238`)
- maxLength khớp spec `:134-137` 100%: MaKeHoach 50 · GhiChu/ContextNote 2000 · Name 500 · Note 1000 · TvgsName 200 · Summary 1000 · SignedByFullName 200 NOT NULL · FileName 500 / StoragePath 1000 / ContentType 200 · Comment(LevelOpinion) 2000 / Comment(Approval) 1000 · HoSoLink 1000.
  Chỉ `FieldChangesJson` = `nvarchar(max)` `:124` (cố ý, JSON tự do).

**Apply 2 DB:**
- `migrations list --connection …SolutionErp_Dev` TRƯỚC apply: chỉ **1 Pending** = Mig 69 ⇒ **0 drift** (khác S133 phải catch-up).
- Dev: `Applying migration '20260729122015_AddContractSigningPlans'. Done.`
- Design (default factory): `Applying migration '20260729122015_AddContractSigningPlans'. Done.`
- `sqlcmd` Dev: `sys.tables WHERE is_ms_shipped=0` = **96** (89→96, +7) · `LIKE 'ContractSigningPlan%'` = **7**.
