# SPEC WAVE-2 — CRUD phiếu nháp + căn cứ b.8-9 + FE 3 page ×2 app (GĐ2 KHKK)

> Nguồn RATIFIED: spec cũ §2.3 (CQRS) + §2.4 (FE 4-place) + §③ WAVE-2 (`:310-339`). Chạy SAU W1.

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

BCH tạo được **phiếu nháp "Kế hoạch ký kết HĐ"** từ PE `DaDuyet`: dòng giá đề xuất per-winner (snapshot `PeReferenceAmount`), danh mục căn cứ b.8-9 (mẫu/shopdrawing TVGS per-item status), đính kèm — **chưa trình được** (trình = W3). Người dùng thấy: 6 leaf `Khkk_*` hết ComingSoon, thành trang thật.

## ② CÁCH IMPLEMENT

1. **Gate 5-anchor** (§③-A).
2. **BE `Application/ContractSigningPlans/ContractSigningPlanFeatures.cs`** (mega-file khuôn `ProposalFeatures.cs` — CHỈ CRUD/DTO, 🔴 CẤM copy phần duyệt của Proposal, bẫy §2.1 spec cũ):
   - `CreateContractSigningPlanCommand(PeId, ApprovalWorkflowId, GhiChu?)`: validator PE tồn tại + `Phase==DaDuyet` (mirror `PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs:53-54` — đường dẫn ĐẦY ĐỦ, file KHÔNG nằm ở Contracts/) + chưa-có-plan-sống (`AnyAsync(p.PeId==x && p.Phase != TuChoi)` → 409; cho tạo lại sau TuChoi) + workflow `ApplicableType==10` (mirror Proposal `:258-267` — copy CẢ guard `:266-267`, đừng chỉ copy phần nạp). Handler auto-sinh Lines từ winners: `pe.Suppliers.Where(IsWinner)`, `PeReferenceAmount = SUM(Quotes WHERE IsSelected)` per-winner (copy phép tính `:56-62,:88-90`) — snapshot 1 lần. Sinh `MaKeHoach` = `KHKK/{YYYY}/{Seq:D3}` (W1 CodeGen).
   - `UpdateContractSigningPlanDraftCommand`: guard `Phase ∈ {DangSoanThao, TraLai}` + cho re-pin `ApprovalWorkflowId`.
   - `Get`/`List`/`pendingMe`: detail Include Lines+DossierItems+Attachments+LevelOpinions+workflow-tree (khuôn Proposal `:161-228`); **inbox V2 precompute TỪ NGÀY 1** (mirror PE `ResolveV2InboxIdsAsync`) — wire sẵn cho W3.
   - `UpsertDossierItemCommand`/`DeleteDossierItemCommand`: chỉ mở `DangSoanThao|TraLai`. Attachment mở mọi phase (triết lý PE S147).
   - `DeleteContractSigningPlanCommand`: allow-list `DangSoanThao|TuChoi` — **mở NGAY W2** (spec cũ M6: thiếu nó, tạo nhầm 1 phiếu là PE bị khoá 409 vĩnh viễn).
   - Màn nguồn "PE đã duyệt — chờ lập KH": query mirror picker `ListApprovedPurchaseEvaluationsQuery:173` đổi vị ngữ `&& chưa-có-plan-sống`.
3. **Controller `ContractSigningPlansController.cs`** — 🔴 authz 2 TẦNG: class `[Authorize]` + per-action `[Authorize(Policy="KeHoachKyKet.{Read|Create|Update|Delete}")]` (Δ W1: prefix `KeHoachKyKet`, KHÔNG phải `ContractSigningPlans` như spec cũ `:202`). Endpoints: GET list/inbox · GET {id} · POST · PUT {id} · dossier-items CRUD · attachments · DELETE {id}. (transitions = W3.)
4. **FE ×2 app** (duplicate CÓ CHỦ ĐÍCH, 4-place mirror Pattern 16-bis): `pages/khkk/` 3 page (List ~252 + Create ~245 + Detail ~386 + types ~95 — khuôn bộ Proposal) · `App.tsx` routes · `Layout.tsx` staticMap **re-point 6 leaf `Khkk_*` khỏi `/coming-soon`** (List→list · Create→create · Pending→list?filter=ChoDuyet · Approved→list?filter=DaDuyet · Deleted→deleted-view · WfView→matrix type=10) · `lib/menuKeys.ts`. Detail: bảng Lines (đề xuất per-NCC) + bảng DossierItems (chip status + TVGS + scan) + attachments; panel workflow + banner = W3.
5. Build ×2 + test + commit.

## ③ CHECKLIST

**A. Gate 5-anchor:** `ProposalFeatures.cs:427-439` (bẫy flatten — để BIẾT mà né) · `CreateContractFromEvaluationFeatures.cs:53-62` (guard+phép tính snapshot) · `ProposalFeatures.cs:258-267` (guard workflow-type trọn) · `DbInitializer.cs:2207` khu staticMap comment `Khkk_*` (leaf đang ComingSoon) · `MenuKeys.cs` All (KeHoachKyKet ĐÃ vào từ W1).
**B. Acceptance (spec cũ W2 `:322-337` + Δ):**
- [ ] `POST /api/contract-signing-plans` từ PE DaDuyet → 201; DB N Lines = số winner (đếm đối chiếu `PurchaseEvaluationSuppliers WHERE IsWinner=1`); `maKeHoach` trả về match `^KHKK/\d{4}/\d{3}$` (format anh OK @S161 — phép đo A6).
- [ ] `PeReferenceAmount` mỗi Line == SUM Quotes IsSelected đúng NCC (2 câu đếm so khớp).
- [ ] Tạo lần 2 cùng PE → 409. · [ ] PE chưa DaDuyet → 400/409, KHÔNG 500.
- [ ] 🔴 Authz 2 tầng: user thiếu quyền POST → **403** (không 200/401/500); có quyền → 201. Gate rẻ: `grep -c "Authorize(Policy" ContractSigningPlansController.cs` ≥ 4.
- [ ] DELETE ở DangSoanThao → 204; sau đó tạo lại plan cho CÙNG PE → 201 (chứng thoát khoá 409).
- [ ] `dotnet test` ≥ baseline + 2, PASS 0 fail, tên khuôn repo (English-predicate): `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` + `CreateContractSigningPlan_FromNonApprovedPe_Throws`.
- [ ] 🔴 Grant ăn (review F-04): sau seed, Permission `KeHoachKyKet` có **CanCreate=1** cho role đích (đo CỜ — row đã tồn tại nên đếm-row 0-bit); user role đó `POST` → 201, user không-cấp → 403 (KHÔNG 500 — chứng policy `KeHoachKyKet.*` sinh đúng, khớp key ∈ All).
- [ ] FE build ×2 PASS · SHA256 file mirror IDENTICAL · 6 leaf `Khkk_*` hết trỏ coming-soon: `grep "Khkk_" fe-user/src/components/Layout.tsx | grep -c "coming-soon"` = 0 (cùng lệnh fe-admin).
- [ ] cicd 3-chân-kiềng + byte-marker bundle (control ÂM độc quyền — chọn chuỗi trước khi đo).
**C. Rủi ro:** [ ] HasQueryFilter hoạt động (phiếu xóa mềm không hiện list) · [ ] tạo-lại-sau-xóa không bị chặn oan (gotcha #57).
