# SPEC WAVE-5 — Cầu KHKK→Hợp đồng + pin V2 (GĐ2→GĐ3, sửa bridge cũ 1 file)

> Nguồn: spec cũ §2.5 + §③ WAVE-5 (`:397-403`) + lane-2 §2 (chi tiết nhất). Anh ĐÃ MỞ gate chạm HĐ @S160 (run.md ④). Defaults plan cha #2 (giá KH thắng) + #3 (Q11 cảnh báo mềm). Chạy SAU W3 + W6.

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

Từ phiếu KHKK `DaDuyet`, tạo HĐ per-winner với **giá = `ApprovedAmount` của đúng NCC** (thay SUM báo giá PE), HĐ **pin workflow V2** để trình được ngay (hết kẹt `ConflictException`). **SỬA BRIDGE CŨ — KHÔNG đẻ endpoint mới** (lane-2 §2c: 2 endpoint cùng nghĩa = lớp lỗi 2-site S155).

## ② CÁCH IMPLEMENT

1. **Gate 5-anchor** (§③-A).
2. **`CreateContractFromEvaluationFeatures.cs`** (file DUY NHẤT phía BE logic):
   (a) đọc plan `DaDuyet` của PE (`Plans.Where(PurchaseEvaluationId==peId && Phase==DaDuyet)`) — CÓ: `giaTri = line.ApprovedAmount` khớp `w.SupplierId` thay `SUM :88-90`, ghi ContextNote lệch *"giá KH x vs SUM-PE y"* vào changelog (khuôn `:121-140`, bài D4 S134); KHÔNG có: giữ SUM + **cảnh báo mềm** trong response (Q11 default).
   (b) `+param ApprovalWorkflowId?` vào command + body record `PurchaseEvaluationsController.cs:379-382` (🔴 record-count controller == command — bài 10-point checklist) → pin V2 cho HĐ tạo ra; không truyền → fallback hiện trạng V1 + warning. 🔴 **GIỮ guard `ApplicableType==Contract` (review F-B6):** cửa-1 chống forge đã có `ContractFeatures.cs:84-86` + comment `:77-78` (chặn attacker forge PE-workflow-id type 1/2); pin ở bridge phải qua CÙNG guard đó — CẤM pin thẳng workflow type 1/2/10 vào HĐ. Test canh `CreateContractCommandApplicableTypeTests` (đã có) + ca ÂM mới.
   (c) 🆕 ghi ngược `Line.ContractId = contract.Id` per-winner trong vòng lặp `:82-115` (cột C6 sẵn từ W1).
   (d) GIỮ nguyên: guard `:53-60` · idempotency `pe.ContractId :145` · atomic codegen `:78-82` · mapping `:101-107`.
3. **FE ×2 app**: màn KHKK Detail (phiếu DaDuyet) thêm nút **"Tạo Hợp đồng"** deep-link gọi CÙNG endpoint `POST /api/purchase-evaluations/{peId}/create-contract` (+field chọn workflow V2 cho HĐ). Màn PE giữ nút cũ (2 đường song song — Q11 default).
4. Test-before 4 test (§③-B) + build ×2 + commit. ⚠️ Đây là commit CHẠM module Contract đầu tiên — cicd verify kỹ (endpoint 401-control-404, khuôn đợt-5).

## ③ CHECKLIST

**A. Gate 5-anchor:** `CreateContractFromEvaluationFeatures.cs:53-60` (guard) · `:68-71` (pin V1-only) · `:88-90` (SUM) · `:145` (idempotency) · `ContractWorkflowService.cs:115-116` (ConflictException kẹt V2-less).
**B. Acceptance (lane-2 §7-W7 + spec cũ W5):**
- [ ] 5 test tồn tại ĐÚNG TÊN (khuôn repo English-predicate) trong `ContractSigningPlanBridgeTests`: `CreateContract_WithApprovedPlan_SetsGiaTri_ToApprovedAmountPerSupplier` · `CreateContract_NoPlan_KeepsSumOfPeQuotes_AndWarnsSoft` · `CreateContract_PinsV2Workflow_ContractReachesLevelOneOnSubmit` · `CreateContract_WritesBackLineContractId_PerWinner` · `CreateContract_ForgePeWorkflowId_ThrowsApplicableTypeGuard` (ca ÂM forge — F-B6).
- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 5; `CreateContractFromEvaluationMultiWinnerTests` + `CreateContractCommandApplicableTypeTests` (ĐÃ TỒN TẠI) VẪN XANH — chống hồi quy multi-winner + forge.
- [ ] TÍNH-CHẤT: PE có W winner + plan DaDuyet → `POST create-contract` trả đúng W contractId; MỖI HĐ `giaTri == ApprovedAmount` của Line cùng supplierId; tổng W HĐ == SUM(ApprovedAmount) toàn plan.
- [ ] 🔴 Prod curl (ZZTEST) — CHẠY TỚI LƯỢT DUYỆT, KHÔNG dừng ở trình (review F-B5: nhánh trình `ContractWorkflowService.cs:70-89` KHÔNG đọc workflow ⇒ "204 KHÔNG 409" PASS SẴN, 0-bit): HĐ tạo với `ApprovalWorkflowId` V2 → `GET /{id}` có **`currentApprovalLevelOrder=1`** (vế có răng — chứng pin V2 ăn); → Cấp-1 approver `POST /{id}/transitions` duyệt → tiến Bước 2. HĐ tạo KHÔNG workflow → trình vẫn 204 (đường cũ) nhưng `currentApprovalLevelOrder=null`.
- [ ] Changelog có ContextNote chứa CẢ 2 số khi giá KH ≠ SUM-PE.
- [ ] FE build ×2 · cicd 3-chân-kiềng.
**C. Rủi ro:** [ ] KHÔNG sửa lệch phép SUM cũ cho PE-không-plan (đường cũ phải y nguyên — test #2 canh) · [ ] body record lệch count controller↔command (silent 400) · [ ] 🔴 dry-run CẤM tạo HĐ `HopDongMuaBan` — không mã "MB" nào được sinh (anh bỏ MB @S161, plan cha default #7; codegen giữ nguyên chờ format thay).
