Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md
2026-07-31 13:39:03 +07:00

22 KiB
Raw Blame History

sub-invest-fable-b2-cum3 — B2 CỤM-3: SPEC CHI TIẾT K7 (cầu KHKK→HĐ + đường ống V2) + K8 (dry-run E2E)

Run 2026-07-31-S164-4gd-khkk-fanout · engine /fable-real · investigator-codebase (read-only spec-writer) Luật đan xen: đọc đủ MỘT section → VIẾT NGAY. Kế thừa S156/S160 — không soi lại vùng đã đo.

K7 Cầu KHKK→HĐ + đường ống V2

K7.0 RE-ĐO 3 nấc (đo lại hôm nay, không tin sổ) + 1 BÁC tiền đề đề bài

Nấc Kết quả đo @S164 Evidence
(a) cột/index ContractSigningPlanLine.cs:25 ContractId Guid? + comment "[C6 review-schema] HĐ sinh ra từ dòng này (W5)"; index non-unique ContractSigningPlanLineConfiguration.cs:26
(b) write-site = 0 (tự grep, không mượn số lead) grep ContractId trong ContractSigningPlanFeatures.cs → 3 hit đều READ: :67 DTO field, :574 projection, :1185 comment; ContractSigningPlanWorkflowService.cs → 1 hit :54 = COMMENT FK. 0 assignment
(c) test E2E = 0 test bridge tests/ có 4 file SigningPlan (Approval/Crud/Schema/AuthorizePolicy) + CreateContractFromEvaluationMultiWinnerTests.cs (bridge PE cũ) — KHÔNG file nào test cầu KHKK→HĐ

🔴 BÁC tiền đề K7(b) của đề bài — "SỬA 3 lỗ đường ống (GetEligiblePhases V2-aware + inbox V2 + guard trình)" là kiến thức S156 ĐÃ LỖI THỜI. Cả 3 đã LAND @W6-S161:

  1. List + Detail V2-aware: ContractFeatures.cs:306-318 — vế (a) myWorkflowIds.Contains(x.c.ApprovalWorkflowId) per-HĐ (helper ResolveUserApprovalWorkflowIdsAsync:403-411) + vế (b) HardCopyActorRoles:391-392 thấy DaPhatHanh. Comment :381-387 CẤM nhét ChoDuyet vào GetEligiblePhases (hàm dùng chung với ListDeleted :441 → nhét là RÒ HĐ ChoDuyet xóa-mềm sang màn "Đã xóa"). K7 KHÔNG sửa GetEligiblePhases — implementer đọc đề bài cũ mà "làm cho xong" là tạo lỗ.
  2. Inbox V2: ContractFeatures.cs:532-534 v2InboxIds = ResolveV2InboxIdsAsync(...) (bản Contract ĐÃ TỒN TẠI) + where :543 || v2InboxIds.Contains(c.Id) + AdminInboxPhases:503-513 đã +ChoDuyet (DR-4).
  3. Guard trình: ContractWorkflowService.cs:73-92 đã nới Drafter DeptManager Procurement isCreator (vá lỗ PMH-403 đo 07-29).

⇒ K7(b) co lại thành V3: test-khóa 3 vế (regression lock — xem K7.4 T8), KHÔNG build lại. Phần build THẬT của K7 = bridge (a) + FE (c).

K7.1 (a) BRIDGE MỚI — POST /api/contract-signing-plans/{id}/create-contract

Khuôn mẫu: mirror bridge PE→HĐ cũ CreateContractFromEvaluationFeatures.cs (endpoint PurchaseEvaluationsController.cs:340-343) + guard workflow V2 của CreateContractCommand (ContractFeatures.cs:74-81). Luật spec-4gd §①: GIỐNG NHAU — CHỈ KHÁC FORM — ĐỪNG CHẾ THÊM.

Command (file mới Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs):

public record CreateContractFromSigningPlanCommand(
    Guid PlanId,
    List<Guid> LineIds,          // OG-1: schema GỘP — N line cùng NCC → 1 HĐ; UI-1-1 truyền 1 phần tử
    ContractType ContractType,
    Guid ApprovalWorkflowId,     // V2 type-3 — user chọn trong dialog
    string? TenHopDong = null) : IRequest<Guid>;   // 1 call = 1 HĐ (khác bridge PE trả List)

⚠️ Lệch đề bài có chủ đích: đề ghi input {planId, lineIds[], contractType}; PHẢI thêm ApprovalWorkflowId vì V2 KHÔNG phân theo ContractType — ApprovalWorkflow.cs:57 Contract = 3, // HĐ general (any ContractType) ⇒ "pin theo ContractType" tự động là BẤT KHẢ; chọn quy trình = việc của dialog (mirror ContractCreatePage dropdown type-3).

Guard tuần tự trong handler (mỗi guard 1 message VN rõ):

  1. plan tồn tại + Phase == DaDuyet (mirror rào (i) ContractSigningPlanFeatures.cs:330-332).
  2. LineIds non-empty, distinct, TẤT CẢ ∈ plan.Lines.
  3. TẤT CẢ line cùng SupplierId — 1 HĐ = 1 NCC (Contract.SupplierId đơn). Liên-danh N NCC = N lần bấm (UI-1-1 mỗi lần 1 line nên tự thỏa). Message: "Các dòng chọn thuộc nhiều NCC khác nhau — mỗi Hợp đồng chỉ 1 NCC, tạo lần lượt."
  4. TỪNG line ApprovedAmount != null — null → Conflict message NÊU TÊN hạng mục: "Dòng '{TenHangMuc}' chưa có giá duyệt chốt — phiếu phải qua finalize (K2-freeze) trước khi đưa vào HĐ." KHÔNG fallback ProposedAmount (số chưa chốt).
  5. TỪNG line ContractId == null — chống double-bridge per-line (thay guard pe.ContractId single của bridge cũ :59-60). Message: "Dòng '{TenHangMuc}' đã có Hợp đồng."
  6. ApprovalWorkflowId: tồn tại + ApplicableType == Contract(3) — copy trọn 2 nhánh NotFound/Conflict theo khuôn EnsureWorkflowTypeAsync (ContractSigningPlanFeatures.cs:209-220) đổi hằng type.

Build Contract (map field): Type=request.ContractType · Phase=DangSoanThao · SupplierId=line.SupplierId · ProjectId=plan.ProjectId (ContractSigningPlan.cs:25) · DepartmentId=plan.DepartmentId (:26) · DrafterUserId=currentUser · GiaTri = Σ ApprovedAmount(lines chọn) — phạm-vi-gộp = ĐÚNG tập LineIds trong call, không hơn không kém (DoD spec-4gd K7) · TenHopDong = request.TenHopDong ?? "{pe.TenGoiThau} — {TenHangMuc}" · ApprovalWorkflowId = request.ApprovalWorkflowId (pin V2 — khác bridge cũ chỉ pin V1 :68-71,108) · WorkflowDefinitionId = null (KHÔNG pin V1 song song — HĐ mới đi V2 thuần, nhánh dispatch ContractWorkflowService.cs:111 ưu tiên V2) · optional BudgetManualName="NS tham chiếu KHKK {MaKeHoach}" + BudgetManualAmount=Σ PeReferenceAmount(lines) (mirror :106-107, tham khảo).

Mã HĐ: gen NGAY lúc bridge — mirror tiền lệ bridge PE :113 + bài S88 self-committing codegen (:78-81): gen TRƯỚC khi Add/track (codegen tự SaveChanges — comment ContractSigningPlanFeatures.cs:352-357 đã trả giá bài này); terminal V2 có guard if (MaHopDong is null) (ContractWorkflowService.cs:384) nên không double-gen. Trade-off cháy seq RG-001 khi HĐ không phát hành → khai ở K8 guard.

Write-site ContractId (nấc b đóng): sau khi Add contract + SaveChanges chung 1 lần: line.ContractId = contract.Id cho TỪNG line chọn (load lines TRACKED — đây là assignment ĐẦU TIÊN toàn codebase). + 2 changelog mirror :117-141: ContractChangelogs (Insert, "Tạo HĐ {mã} từ KHKK {MaKeHoach}") + ContractSigningPlanChangelogs (Update, ContextNote=contractId). Atomic: 1 SaveChanges cuối.

Nối cây GĐ3 (quyết): pe.ContractId ??= contract.Id (chỉ khi null) — usePipelineStages.ts đi GĐ3 DUY NHẤT qua pe.contractId (skill contract-workflow đã khai); không set = HĐ sinh ra NHƯNG cây 4-folder mù GĐ3. Hệ quả 2 chiều khai rõ: (i) bridge PE→HĐ cũ sẽ 409 cho PE đó (:59-60 — đúng ý, khóa đường cũ tự nhiên); (ii) liên-danh N-HĐ cây vẫn chỉ link HĐ[0] — hố sẵn có của bridge cũ (:145), nâng cấp cây đọc line.contractId = backlog W9, KHÔNG làm đợt này (đụng 2 file SHA-identical ràng buộc K5).

Controller: ContractSigningPlansController[HttpPost("{id:guid}/create-contract")] [Authorize(Policy = "Contracts.Create")]🔴 đúng-key-policy-endpoint-đích (gotcha #85): tài nguyên sinh ra là Contract ⇒ key Contracts.Create, KHÔNG phải KeHoachKyKet.*. Trả 201 { contractId, maHopDong }.

Ai bấm + chọn ContractType (câu III): PENDING-OWNER — default spec: "người có quyền tạo HĐ" = ai mang policy Contracts.Create thấy nút + tự chọn ContractType trong dialog (khớp l1-F15 spec-4gd "mặc định người bấm chọn trong dialog"). Đánh dấu chờ anh; đổi answer chỉ đổi điều kiện hiện nút FE, máy BE không đổi.

K7.2 (b) Đường ống — trạng thái sau re-đo

  • ĐÃ VÁ (K7.0): list/detail/inbox/guard-trình. Việc K7 = V3 test-khóa + xác nhận runtime bằng curl trong acceptance (A3/A4).
  • CÒN THIẾU thật: 0 — HĐ pin V2 sinh từ bridge đi được trọn: trình (:70-101 set ChoDuyet + levelOrder=1 vì AWId≠null :97) → ApproveV2Async (:111-113) → terminal gen mã + DaPhatHanh (:381-392).
  • ⚠️ Tiền đề vận hành (không phải code): prod phải có ≥1 ApprovalWorkflow ApplicableType=3 active + roster thật. Dev hiện chỉ có QT-HD-V2-001 "mẫu UAT" (sqlcmd Dev @S164); prod CHƯA đo được (SSH chết khi load SQL-client — S134/S148) → chuyển thành pre-flight K8 B0.

K7.3 (c) Nút FE ×2 app

  • Vị trí: fe-user/src/pages/khkk/KhkkDetailPage.tsx khối actions PageHeader :291-318 (cạnh nút Xóa/Danh sách) + mirror fe-admin/.../KhkkDetailPage.tsx (tiền lệ PeWorkflowPanel byte-identical 2 app — S155).
  • Điều kiện hiện: phase === KhkkPhase.DaDuyet && can('Contracts','Create') (#85 — đúng key endpoint; KHÔNG OR Khkk_*).
  • Dialog "Đưa vào Hợp đồng": (1) chọn 1 line (UI-1-1 OG-1 — radio list các line contractId == null, hiện TenHangMuc + NCC + ApprovedAmount; line đã bridge → badge "Đã có HĐ" + link, disabled); (2) Select ContractType 7 enum; (3) Select Quy trình duyệt HĐ (nguồn = mirror dropdown ContractCreatePage, filter ApplicableType=3); (4) TenHopDong prefill. Submit → POST → toast mã HĐ + navigate/link HĐ; invalidate query detail.
  • Line có ApprovedAmount == null → disabled + tooltip "chưa có giá chốt".

K7.4 (d) Test — 7 case (fixture khuôn ContractSigningPlanApprovalTests.cs sẵn có)

# Case Assert
T1 Happy 1-line: plan DaDuyet, line ApprovedAmount=X 1 HĐ: GiaTri==X · Phase=DangSoanThao · ApprovalWorkflowId pin đúng · WorkflowDefinitionId null · MaHopDong non-null · line.ContractId==hđ.Id · pe.ContractId==hđ.Id · 2 changelog
T2 Gộp N=2 line CÙNG NCC (máy OG-1) GiaTri==Σ 2 ApprovedAmount · CẢ 2 line cùng ContractId
T3 1 line ApprovedAmount=null Conflict, message chứa TenHangMuc; DB 0 HĐ mới + 0 line đổi (atomic)
T4 plan.Phase=ChoDuyet Conflict "chỉ từ phiếu Đã duyệt"
T5 line đã có ContractId Conflict (double-bridge per-line)
T6 2 line KHÁC SupplierId 1 call Conflict "mỗi HĐ 1 NCC"
T7 ApprovalWorkflowId type-10 (KHKK) Conflict (guard type-3 — chống forge, mirror :74-81)
T8 (khóa V3) HĐ pin V2 ở ChoDuyet: approver-V2-không-role-legacy thấy List (vế :315-316) + Inbox (:543); cấp-2 CHƯA thấy inbox khi đang cấp-1

K7.5 (e) Acceptance đo được (chạy sau deploy, số cụ thể)

  • A1 POST .../create-contract → 201; sqlcmd SELECT ContractId FROM ContractSigningPlanLines WHERE Id IN (…) = đúng contractId, đúng N dòng chọn.
  • A2 sqlcmd Contracts.GiaTri == Σ ApprovedAmount các line chọn — khớp từng đồng.
  • A3 login user CHỈ có chân V2 (0 role legacy): GET /api/contracts?phase=10 + GET /api/contracts/{id} → thấy HĐ (trước W6: rỗng).
  • A4 GET /api/contracts/inbox: approver cấp-hiện-tại CÓ HĐ; user cấp-sau CHƯA.
  • A5 duyệt hết trạm V2 → Phase=9 (DaPhatHanh); MaHopDong đúng RG-001 ({ProjectCode}/{abbr}/SOL&{SupCode}/{Seq:D2}) — sinh từ lúc bridge, terminal không double-gen.
  • A6 POST lần 2 cùng line → 409; FE line badge "Đã có HĐ" + disabled.
  • A7 dotnet test ≥ baseline+8 (khai k con số); cicd PASS; FE-ship → byte-verify #77 ×2 app.

K7.6 Danh sách việc (n = 8)

V1 BE command+validator+handler bridge (file mới) · V2 BE endpoint controller + policy Contracts.Create · V3 test-khóa đường ống (T8 — KHÔNG sửa GetEligiblePhases) · V4 FE fe-user nút+dialog · V5 FE fe-admin mirror · V6 FE types/hook + invalidate (×2 app) · V7 test T1-T7 · V8 acceptance A1-A7 + khai số test.

K8 Dry-run E2E

K8.0 Tham số chốt + 2 QUYẾT

  • Nguồn: PE/2026/A/049 (gói 14 Mat PVC) — theo đề bài. QUYẾT-1 nhóm: ống PVC = vật tư chính ⇒ nhóm B1 · Vật tư → workflow KHKK-N4 (transcribe danh-muc-sp002-transcribe.md:52 "B1 · Vật tư / Materials (23) → N4"; hàng khớp chọn trong 23 dòng B1, fallback B1-23 Vật tư chính khác :75). KHÔNG phải A1 (:20 = thiết bị/vật tư PHỤ → N1).
  • QUYẾT-2 loại HĐ: HopDongNhaCungCap (abbr "NCC" CÓ trong RG-001 v02 — né hố "MB" không gốc quy định, S160 F-06.1/F-21).
  • Đội hình: cột "Ai" = chép từ bảng danh tính K3-acceptance (vá-11 cụm-1, đo trên bản LIVE) — Dev KHÔNG có QT-DN-V2-001 (sqlcmd Dev @S164 chỉ có QT-HD-V2-001 type-3 UAT) ⇒ KHÔNG điền trước email từ Dev. Anchor đã chốt: trạm CCM = TP.CCM Phan Văn Chương (finalize-gate OG-3/OG-9) · trạm cuối = CEO (anh Trường). Giả định 4 trạm — số trạm THẬT theo seed K3, người chạy bám panel duyệt.
  • Nhánh chính = A (finalize tại a Chương) — chứng tính năng MỚI OG-9; nhánh B (lên CEO) = phiếu ZZTEST thứ 2 nếu còn giờ.
  • Login API prod: parse field accessToken (KHÔNG phải token — đo S160).

K8.1 Bảng bước (B0-B16 = 17 bước; mỗi bước Ai · Hành động · Kỳ vọng · Bằng chứng)

B0 PRE-FLIGHT (chặn cả buổi nếu thiếu): (i) scripts/backup-sql.ps1 backup prod; (ii) verify prod có ApprovalWorkflow ApplicableType=3 ACTIVE (đội hình HĐ thật) + KHKK-N4 active + roster khớp bảng K3 — thiếu → admin tạo bằng designer fe-admin TRƯỚC; (iii) verify PE/2026/A/049: Phase=DaDuyet + có winner IsWinner + chưa có plan nhóm-4 sống (rào ContractSigningPlanFeatures.cs:342-347, sau K2 = per-(PeId,group)); (iv) dặn team (Zalo/email): "hệ thống bắn notification phiếu ZZTEST trong khung giờ X — bỏ qua" — KHÔNG có cơ chế tắt notify per-record, chỉ dặn (DoD l3-F16); (v) hỏi anh câu giờ-G ở K8.2. Bằng chứng: ảnh backup + sqlcmd 3 dòng verify.

# Ai Hành động Kỳ vọng UI/API Bằng chứng
B1 BCH drafter [K3-bảng] fe-user /khkk → Tạo mới: picker chọn PE/2026/A/049, workflow KHKK-N4, GhiChu tiền tố ZZTEST 201 + MaKeHoach = KHKK/2026/{seq} hiện header ss-k8-01 + mã phiếu
B2 BCH drafter Điền line: gán hạng mục B1-xx (PVC) per line, ProposedAmount per NCC×hạng mục; save Lines lưu; line thiếu hạng mục bị submit-guard chặn (vá-5 cụm-1) ss-k8-02
B3 BCH drafter Trình duyệt (Nháp→ChoDuyet) Badge "Chờ duyệt"; phiếu vào inbox trạm-1; notification bắn ss-k8-03
B4 Trạm-1 [K3-bảng] Login → /khkk Đang duyệt → mở phiếu → KhkkWorkflowPanel bấm Duyệt + ý kiến "ZZTEST duyệt trạm 1" Advance cấp/bước; LevelOpinion UPSERT hiện Section ý kiến ss-k8-04
B5 Trạm-2 [K3-bảng] Như B4 tại trạm 2 Advance; user trạm-1 hết thấy phiếu trong inbox ss-k8-05
B6 TP.CCM Phan Văn Chương Trạm CCM: tick "Duyệt kết thúc tại cấp" (AllowApproverFinalize — OG-9 nhánh A) Phase=DaDuyet KHÔNG qua CEO; EndedByLevelFinalize=true; ApprovedAmount ghi tại choke-point + FREEZE (QĐ9 mirror Mig 67) ss-k8-06 + sqlcmd EndedByLevelFinalize,ApprovedAmount
B7 (chỉ nhánh B — phiếu ZZTEST-2) CEO Trường Duyệt final trạm cuối Phase=DaDuyet đường chuẩn ss-k8-07
B8 BCH/Admin Mở lại detail: số khóa 🔒; thử sửa line ApprovedAmount hiển thị khóa; sửa line bị phase-guard chặn (vá-6 cụm-1); sqlcmd số không đổi ss-k8-08 + sqlcmd
B9 Người có Contracts.Create [ câu III] Bấm "Đưa vào Hợp đồng" (K7): dialog chọn 1 line (UI-1-1) + ContractType=NhaCungCap + workflow HĐ type-3 201 → HĐ draft GiaTri == ApprovedAmount line; MaHopDong RG-001 có ngay; line badge "Đã có HĐ" ss-k8-09 + mã HĐ
B10 Người bấm B9 Mở HĐ draft (TenHopDong tiền tố ZZTEST), kiểm NCC/dự án/giá → Trình duyệt Phase→ChoDuyet (guard trình đã nới :73-92); vào inbox approver V2 HĐ ss-k8-10
B11 Approver HĐ [đội hình type-3] Duyệt lần lượt hết trạm V2 Terminal: Phase=DaPhatHanh (mã giữ nguyên — guard :384); notification "đã phát hành" ss-k8-11
B12 HRA/CCM [HardCopyActorRoles] GĐ4: mở HĐ (leaf Hdc_*) → upload scan bản cứng (purpose SealedCopy) Badge hasSealedCopy (EXISTS ContractFeatures.cs:348-355); vai bản-cứng THẤY HĐ DaPhatHanh (:317-318) ss-k8-12
B13 Bất kỳ + Admin Mở cây 4-folder (usePipelineStages) Đủ 4 tầng: PE A/049 → KHKK ZZTEST (folder nhóm 4) → HĐ ZZTEST → bản cứng; đường nối khkk.purchaseEvaluationId · pe.contractId · hasSealedCopy ss-k8-13
B14 Lead Chốt sổ: bảng mã thật (PE/2026/A/049 · KHKK/2026/xxx · mã HĐ) + tick DoD K8 spec-4gd :35 Đủ: ZZTEST prefix · toàn trình hết trạm · cây 4 GĐ · screenshot + mã thật bảng trong session-log

B15 CLEANUP + danh sách bản-ghi-KHÔNG-xóa-được (sống chung):

  • HĐ ZZTEST DaPhatHanh — HỐ-1: DeleteContract guard numeric Phase >= DangInKy (ContractFeatures.cs:632-633, S160) chặn xóa cả 9/10/98/99, admin không thoát qua API. Sống chung: giữ vĩnh viễn với tiền tố ZZTEST, HOẶC sqlcmd soft-delete tay (IsDeleted=1) sau khi anh gật.
  • KHKK ZZTEST DaDuyet🔴 HỐ-1-KHKK (đo mới @S164): delete allow-list {DangSoanThao, TuChoi} nằm SAU nhánh admin (ContractSigningPlanFeatures.cs:1145-1148) ⇒ phiếu DaDuyet KHÔNG xóa được KỂ CẢ Admin + chiếm slot (A/049, nhóm-4) — PE thật này hết lập phiếu N4 cho tới khi xử. Lối thoát: sqlcmd UPDATE ContractSigningPlans SET IsDeleted=1,… — global filter làm rào :342-344 giải phóng slot (soft-deleted không tính "sống").
  • Seq đã cháy: ContractCodeSequences (mã HĐ ăn STT 01 của cặp prefix (dự án thật, NCC thật)) + KHKK/2026 seq — không thu hồi.
  • Changelog/Approval/LevelOpinion các phiếu trên + notification đã bắn — vĩnh viễn by-design.
  • PE A/049: KHÔNG đụng số liệu (chỉ bị set pe.ContractId bởi bridge B9 — khai với anh; muốn gỡ = sqlcmd NULL lại sau khi soft-delete HĐ).

B16 ROLLBACK: hỏng giữa chừng → DỪNG, không xóa gì, chụp lỗi + ghi bước đứng; phiếu KHKK kẹt ChoDuyet → approver "Từ chối" (TuChoi → xóa được qua UI :1145); HĐ kẹt ChoDuyet → admin Reject→TuChoi; restore DB = last-resort KHÔNG dùng (prod đang sống, restore = mất data người khác — S91).

K8.2 Câu hỏi giờ-G cho anh (1 câu, trước B1)

Vòng-1 chạy thẳng trên PE/2026/A/049 thật (giá: KHKK ZZTEST DaDuyet + HĐ ZZTEST DaPhatHanh mang tên dự án/NCC THẬT nằm vĩnh viễn trong list tới khi anh cho sqlcmd soft-delete + cháy 1 seq mã HĐ của cặp prefix thật) — HAY dựng bộ ZZTEST riêng (ZZTEST Project + Supplier + PE tự tạo từ GĐ1, 0 đụng data thật, thêm ~30 phút setup)? Đề bài chỉ định A/049 ⇒ default = A/049 nếu anh im lặng; "PE test riêng" = vòng-2 (đề bài đã chừa).

Thứ tự + phụ thuộc

K1 ──→ K2 ──→ K3 ──────────────────────┐
        │        │                      ▼
        │        └─ phiếu duyệt được → DaDuyet + ApprovedAmount (port finalize)
        └─ TenHangMuc/ApprovalGroup/freeze-răng            K7 (V1→V2→V7 BE; V3∥; V4/V5/V6 FE; V8 sau deploy)
cụm-2: K4a ∥ ngay · K4b/K4c sau K2/K3 · K5 ∥ · K6 ∥         │
                                                            ▼
                          K8 = CUỐI CÙNG (sau MỌI wave deploy prod + cicd PASS + B0)
  • K7 SAU cụm-1, lý do đo được (3): (i) message-chặn T3 cần TenHangMuc — cột chỉ có sau Mig 71 K2; (ii) guard/rào per-(PeId, ApprovalGroup) + phase-guard line = K2 vá-6, K7 build trên trạng thái đó; (iii) không có K3 thì ApprovedAmount NULL 100% (cột "CHỈ ghi tại choke-point finalize" Line.cs:23 — nhánh finalize là việc K3.a) ⇒ bridge chặn mọi phiếu, K7 không test nổi happy-path.
  • K7 ⊥ cụm-2: không cần K4/K5/K6 về máy (điểm chạm cây duy nhất = set pe.ContractId, không đổi file cây).
  • Rebase: K7 thêm code vào ContractSigningPlanFeatures.cs + FE KhkkDetailPage — cụm-1 vá 2/6/8 chạm CÙNG file ⇒ K7 khởi công SAU khi cụm-1 merge, đọc lại spec-cum1 §② trước khi viết (đừng vá lại :342 — vá-2 đã CẤM).
  • K8 sau TẤT CẢ: dry-run đi qua menu 48-leaf (K4), cây sub-folder (K5), admin thu gọn (K6), designer 8 nhóm (K3), bridge (K7) — thiếu wave nào lộ wave đó; + B0 pre-flight prod.

Checklist tổng cụm-3

  • K7-BE: POST bridge 201 · Line.ContractId ghi (write-site ĐẦU TIÊN toàn codebase) · pin ApprovalWorkflowId type-3 · GiaTri == Σ ApprovedAmount tập line chọn · null → chặn message CÓ TÊN hạng mục · double-bridge per-line 409 · policy Contracts.Create (#85 đúng-key-endpoint).
  • K7-KHÔNG-LÀM (chống làm-thừa-tạo-lỗ): GetEligiblePhases GIỮ NGUYÊN (ContractFeatures.cs:381-387 cấm nhét ChoDuyet) · ListDeleted GIỮ NGUYÊN (:442-447 cố ý không ghép vế V2).
  • K7-FE ×2 app: nút chỉ DaDuyet && can('Contracts','Create') · dialog 1-line UI-1-1 · badge "Đã có HĐ" · byte-verify #77 (wave có FE-ship).
  • K7-test: T1-T8 xanh, suite ≥ baseline+8 (khai k), cicd PASS.
  • K7-acceptance: A1-A7 số thật (curl + sqlcmd), dán vào PR.
  • K8-pre: B0 đủ 5 mục + câu giờ-G (K8.2) có trả lời.
  • K8-run: B1-B14 đủ bằng chứng (ss-k8-01..13 + bảng 3 mã thật PE/KHKK/HĐ).
  • K8-hậu: B15 danh sách bản-ghi-vĩnh-viễn ghi vào session-log (gồm HỐ-1-KHKK mới) · B16 không kích hoạt hoặc ghi lý do.
  • DoD gốc spec-4gd: K7 :34 + K8 :35 tick từng dòng — không tick gộp.

END sub-invest-fable-b2-cum3 — TOTAL: K7 8v · K8 17-bước · 8 test