# PLAN CHA — DRY-RUN TOÀN TRÌNH ĐẾN HỢP ĐỒNG CỨNG (S160, 2026-07-29)

> **Lệnh anh (verbatim):** `run.md` §"LỆNH ANH". Tóm: dry-run luôn đến Hợp đồng cứng · spec KHKK cũ ĐÚNG (ratified) · ra Plan cha + mỗi wave 1 spec-chi-tiết-cách-làm + checklist · `/fable-clone` review · `/fable-real` review chốt · hmw Opus-5-MAX chạy từng wave theo Plan cha + kiểm.
> **Nguồn:** spec cũ RATIFIED `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md` (+NOTE W1 @S160) · lane-1 `sub-investigator-codebase-1.md` (25 mục F-01→F-24) · lane-2 `sub-investigator-codebase-2.md` (8 mục §0-§7) · đo tươi @S160 (lead): prod Contracts **0 active + 0 deleted** (admin-bypass verify `ContractFeatures.cs:298/:440`).
> **Trạng thái:** CHỜ `/fable-clone reviewer` + `/fable-real reviewer` chốt cuối → rồi hmw thi công.

---

## 1. RANH + NHÃN (chốt @S159, spec cũ `:27` — CẤM tự chế lại)

- **GĐ1** Duyệt NCC = PE (live) · **GĐ2** Kế hoạch ký kết HĐ = b.7→12 (b.7 email BỎ — Q8 ĐÓNG) · **GĐ3** Duyệt hợp đồng = **b.13→18** (tận dụng Contract V2) · **GĐ4** Hợp đồng cứng = **b.19→21, ký cứng UPLOAD-ONLY**.
- Khúc b.13→21 = span verdict-LAI (toàn bộ tái dùng Contract), KHÔNG phải span GĐ3 (lane-1 F-09 bắt drift này trong chính run.md).
- **Nhãn GĐ2 2 đời:** "Đề xuất ký kết hợp đồng" (sáng @S159, còn trong spec cũ `:27`) → **superseded "Kế hoạch ký kết HĐ"** (chốt cuối @S159, prod `Khkk_G1` seeder). Mọi label VN mới = **"Kế hoạch ký kết HĐ"**; class/bảng English `ContractSigningPlan*` GIỮ.
- Nuance lane-1 F-11: **bản cứng vật lý SINH ở b.17-18 (∈GĐ3)**. 🔄 **@S161 anh rút GĐ4 còn 1 MỐC hệ-thống** (default #8): b.19/20/21 làm trên giấy, hệ thống chỉ ghi nhận **upload scan bộ cứng = xong**; khối readonly GĐ3 chỉ còn LevelOpinions (duyệt điện tử), KHÔNG attachment ký nháy.

## 2. BỨC TRANH WAVE (hợp nhất 2 lane — đánh số GIỮ W1-W5 của spec ratified, nối W6-W8)

| Wave | Tên | GĐ | Mig? | Độ lớn | Spec file | Trạng thái |
|---|---|---|---|---|---|---|
| **W1** | Schema + permission + Designer type-10 | GĐ2 | ✅ **Mig 69** (7 bảng — mig DUY NHẤT toàn plan) | BE nặng | `spec-wave-w1-*.md` | sẵn chạy |
| **W2** | CRUD phiếu nháp + căn cứ b.8-9 + FE 3 page ×2 app | GĐ2 | ❌ | FE nặng | `spec-wave-w2-*.md` | sẵn chạy (sau W1) |
| **W3** | Duyệt 3 trạm + finalize chốt giá 🔴 nặng nhất | GĐ2 | ❌ | BE nặng | `spec-wave-w3-*.md` | sẵn chạy (sau W2) |
| ~~W4~~ | Gate + checklist per-phòng FO-002.01 | GĐ2 | ❌ (1 mig nếu số hoá checkbox) | — | **KHÔNG có spec — DEFERRED** | ⛔ chặn O-Q1/O-Q2; dry-run chạy default (comment tự do + cảnh báo mềm, 0 dev — lane-1 F-18) |
| **W5** | Cầu KHKK→HĐ + pin V2 (sửa bridge cũ 1 file) | GĐ2→3 | ❌ | 2 BE + 2 FE | `spec-wave-w5-*.md` | anh ĐÃ MỞ gate chạm HĐ @S160 |
| **W6** | Đường ống HĐ V2 (view + inbox + vai-trình) | GĐ3 | ❌ | 2-3 BE | `spec-wave-w6-*.md` | sẵn chạy (độc lập W1-W3) |
| **W7** | Bản cứng `Hdc_*` 7 leaf trang thật | GĐ4 | ❌ (option A derive; B = 1 mig nếu anh muốn field tường minh) | FE nặng + 1-2 BE | `spec-wave-w7-*.md` | sẵn chạy (build song song; verify cần HĐ phase-9 từ W5/W6) |
| ~~W8~~ | ~~Authz khoanh vùng Contract~~ | cross | — | — | `spec-wave-w8-*.md` (= biên bản quyết định) | ⛔ **GỠ KHỎI DRY-RUN — anh chốt O-A @S160** (để mở, không siết; đúng ❹ "hiển thị hết để góp ý" + O-3 "hợp đồng cứ từ từ"). Rủi ro (Reject-trước-guard + attachment IDOR + Create ăn số) = **chấp nhận có ý thức**, khoanh bằng ZZTEST + backup DB (§4) |
| ~~W9~~ | Hồ sơ hoàn tất + thống kê lưu trữ | GĐ4 | ❌ | — | **KHÔNG có spec — DEFERRED** | không chặn vòng chạy (lane-1 F-19) |

**Thứ tự thi công (đồ thị phụ thuộc):**
```
W1 → W2 → W3 ──┐              (GĐ2: phiếu KHKK sống trọn vòng)
               ├→ W5 → [E2E]  (cầu: HĐ sinh từ KHKK, pin V2, hết kẹt ConflictException)
W6 ───────────┘              (đường ống HĐ V2: mở view+inbox ChoDuyet (GĐ3 approver) VÀ view
                              DaPhatHanh phase-9 (GĐ4 actor bản-cứng) — PHẢI trước W5/W7 VẬN HÀNH;
                              build song song W1-W3 được vì 0 đụng file KHKK)
W7 (build song song từ đầu; 🔴 VERIFY PHỤ THUỘC W6-view — không có W6 mở phase-9 thì trang
    Bảng cứng RỖNG + 403 cho HRA/BOD/CCM non-admin: F-C2/F-01)
W8: GỠ (O-A — anh chốt để mở @S160)
```
**Dry-run E2E đạt khi:** W1+W2+W3+W6+W5+W7 xong — 1 phiếu PE `DaDuyet` (dự án ZZTEST) → phiếu KHKK → 3 trạm duyệt → HĐ per-winner pin V2 → 3 trạm HĐ → `DaPhatHanh` → trang Bảng cứng upload scan ký/dấu/lưu. Toàn bộ **1 migration duy nhất (Mig 69, W1)** — GĐ3+GĐ4 = 0 mig (lane-2 §6).
🔴 **CẠNH ĐỒ THỊ THÊM (review F-C2/F-01/F-06):** W7-verify đòi W6-đã-mở-view-phase-9; b.14-16 (soạn/góp ý/đàm phán) = trạng thái Nháp HĐ + comment + update-draft trong W5/W6 (KHÔNG wave riêng — default #13). **Cần dựng 1 ApprovalWorkflow type=3 THẬT 3-trạm qua Designer** (seed chỉ 1 trạm — §4 bước mới).

## 3. BASELINE ĐO TƯƠI @S160 (căn mọi acceptance vào đây, KHÔNG chép số cũ)

- Prod: **Contracts = 0 active + 0 deleted** (curl admin 6 phase + tổng + /deleted; admin bypass `ContractFeatures.cs:298`) — sổ S156 "7 HĐ V1 thật" đã STALE. ⇒ lỗ Reject chưa có nạn nhân thật; anh chốt O-A (để mở) ⇒ **W8 GỠ**, rủi ro khoanh bằng ZZTEST + backup.
- Menu 142 key (root 12) · `Khkk_*` = 7 key (G1 + 6 leaf, CanRead 13/13, leaf → `/coming-soon`) · `Hdc_*` = **7 leaf per-ContractType** (KHÔNG phải 6-leaf — lane-2 §3 sửa grounding) · `Ct_*` 21 key mới.
- Test **562** (45D+517I) — acceptance neo TÍNH-CHẤT: "PASS 0 fail + số-sau ≥ số-trước-đo-ngay-trước-wave + N + tên test tồn tại", CẤM literal "562+N" (lane-2 §7).
- Mig cuối = 68 · 89 bảng · mig kế = **69** (chỉ W1).
- Bundle: admin `D0sXA0fe`/`DWDbm5As` · user `Y6dW_5CM`/`6YIAufJR`.
- `MenuKeys.KeHoachKyKet` const CÓ (`MenuKeys.cs:41` + comment dặn tái dùng) nhưng **NGOÀI `All`** ⇒ 0 policy `KeHoachKyKet.*` runtime (đo @S160). `Contracts` ∈ All `:168` ⇒ 4 policy `Contracts.*` sống sẵn.

## 4. CHIẾN LƯỢC DATA DRY-RUN (lane-2 §4 + đo mới)

1. **Dự án test `ZZTEST` + 1-2 NCC test** tạo qua UI (0 code): mã HĐ dry-run ăn sequence per-prefix riêng (`ContractCodeGenerator.cs:33,:40-53`) — không đụng chuỗi số dự án thật; NGOÀI seed-list ⇒ xóa mềm không bị re-seed (#75/#76). **CẤM đặt Code trùng seed-list** (twin FLOCK-01). 🔴 **CẤM tạo HĐ loại `HopDongMuaBan`** (anh bỏ "MB" @S161 — default #7).
2. **PE side có data THẬT** — phiếu KHKK dry-run tạo từ PE test mới (mỗi vòng thử cần PE mới vì idempotency `pe.ContractId` `:59-60,:145`).
2-bis. 🔴 **DỰNG WORKFLOW type=3 3-TRẠM (review F-02 — bắt buộc E2E):** seed prod chỉ có `QT-HD-V2-001` = **1 trạm CCM** (`DbInitializer.cs:239-265`, approver binh.le). E2E hứa 3 trạm (PRO→CCM→CEO) + b.19 BOD ⇒ **admin phải tự dựng 1 ApprovalWorkflow `ApplicableType=3` 3 Bước qua Designer** (0 code — Designer type-3 đã có) TRƯỚC khi chạy W5. Tương tự KHKK type=10 cần 1 workflow 3-trạm (W1 Designer). Ghi là **bước setup thủ công**, không phải code wave.
3. **KHÔNG cờ IsDryRun** (blast cardinality mọi list/inbox — bài S87/S88).
4. **Hố rollback khai trước:** HĐ ≥ phase 5 KHÔNG xóa được qua API (`ContractFeatures.cs:632-633` so sánh SỐ) → HĐ test DaPhatHanh nằm lại (lọc bằng ZZTEST) hoặc admin-override về DangSoanThao (`ContractWorkflowService.cs:191-198`) rồi xóa. Phiếu KHKK/PE DaDuyet cũng không xóa (allow-list). Sequence không rollback (gap số chấp nhận — comment repo `:113-115`).
5. **Notification bắn cho user thật** (in-app, không tắt per-record) — dặn team trước đợt dry-run.
6. **Backup DB trước đợt** (`scripts/backup-sql.ps1`).
7. 🔴 **HỐ AUTHZ CHẤP NHẬN CÓ Ý THỨC (O-A anh chốt @S160 — review F-S2/F-O1) — disclose để không ai tưởng đã kín:** vì để mở, trong dry-run **mọi user đăng nhập**: (i) TraLai/TuChoi **bất kỳ HĐ bất kỳ phase** (Reject-trước-guard `ContractWorkflowService.cs:49-66`); (ii) **upload/xóa/tải attachment** (kể cả scan đã ký) của HĐ bất kỳ — 3 handler `ContractAttachmentFeatures.cs:56-152` **0 IDOR + 0 phase-guard**; (iii) Create HĐ ăn sequence thật nếu chọn dự án thật. ⇒ **CHỈ chạy dry-run trên ZZTEST + backup trước**; đây là rủi ro anh nhận, KHÔNG phải lỗ chưa biết. *(Khác KHKK GĐ2: module MỚI vẫn build authz 2-tầng đúng #82 — "để mở" chỉ áp module Contract sẵn có.)*

## 5. BẢNG DEFAULT 13 QUYẾT ĐỊNH (dry-run chạy bằng default ĐẢO ĐƯỢC — anh veto lúc nào cũng kịp; #11 ĐÃ RESOLVED O-A)

| # | Câu treo | DEFAULT dry-run | Đảo thế nào | Nguồn |
|---|---|---|---|---|
| 1 | Q3 điều kiện rẽ b.12 | KHÔNG rẽ sớm — đường THƯỜNG 3 trạm, CEO ký thật (2 đường sớm anh từng phán "PHÁ VỠ" S155) | bật cờ workflow (0 code) | spec cũ `:67-71`, W3 `:362-365` |
| 2 | Q6 giá vào HĐ | `ApprovedAmount` KH **thắng** + ContextNote lệch vào changelog | đổi 1 nhánh if trong bridge | spec cũ §2.5 + lane-2 §2(b) |
| 3 | Q11 tạo HĐ khi chưa có KH | **cảnh báo mềm** — 2 đường song song (PE→HĐ tắt vẫn sống) | nâng 409 chặn cứng | lane-2 §2(d) |
| 4 | O-Q1 căn cứ thiếu TvgsDuyet | cảnh báo mềm khi trình | 409 | spec cũ W4 |
| 5 | O-Q2 checkbox per-phòng | comment tự do LevelOpinions (W4 DEFERRED, 0 dev) | số hoá checkbox (wave riêng) | lane-1 F-18 |
| 6 | Mã phiếu KHKK | ✅ **RESOLVED — anh OK @S161:** `KHKK/{YYYY}/{Seq:D3}` qua `WorkflowAppCodeSequence` (RG-001 v02 CÂM về phiếu); phép đo format thêm @W1 §③-B + W2 §③-B (review A6: quyết owner-visible phải có acceptance) | đổi format 1 chỗ CodeGen | lane-1 F-07 + spec cũ `:128` + owner @S161 |
| 7 | Viết tắt "MB" (Mua bán) — codegen tự chế, KHÔNG có trong RG-001 v02 | ✅ **RESOLVED — anh chốt @S161 (verbatim): "Bỏ cái MB đi"** ⇒ (i) dry-run **CẤM tạo HĐ `HopDongMuaBan`** — không mã MB nào được sinh (guard W5 §③-C); (ii) `ContractCodeGenerator.cs:25` "MB" = **nợ đã tuyên án**, format thay thế (PO-format theo comment `:18` hay khác) làm CHUNG đợt sửa Contract service — O-3 "cứ từ từ" còn hiệu lực, KHÔNG đổi codegen giữa dry-run | đổi 1 case switch khi anh chốt format thay | lane-1 F-06/F-21 + owner @S161 |
| 8 | GĐ4 vật mang | ✅ **RESOLVED — anh chốt @S161 (verbatim):** *"trình ký sẽ hiển thị full form doc có sẵn trong mục FORM -> điền đầy đủ vào luôn -> Duyệt qua (Nếu có) -> Upload file cứng lên -> Là xem như xong"* ⇒ **A-RÚT-GỌN 1-MỐC**: 0-mig **+ 0 enum-extend** — badge duy nhất "Đã lưu bản cứng" = has(`SealedCopy` SẴN); ~~InitialSigned=4/CoverChecklist=5~~ BỎ; b.19/b.20 diễn ra NGOÀI hệ thống (vẫn KHÔNG `ScannedSigned` — F-C6/gotcha #71). Option B RETIRED | nâng field 4-mốc nếu vận hành thật đòi — backfill từ attachment sẵn | lane-2 §3 + owner @S161 |
| 9 | Số bộ gốc / nơi lưu / ngày giao | note **TÙY CHỌN** khi upload SealedCopy (hạ từ "bắt buộc" theo 1-mốc @S161 — "upload = xong", không chặn thao tác) | option B field tường minh | lane-1 F-13/F-14 (nguồn câm) |
| 10 | Thời điểm gán mã HĐ (QT đòi b.17, máy gen ở create + terminal) | **GIỮ máy hiện trạng** (create-time `ContractFeatures.cs:113-118` — có mã TRƯỚC khi in, sớm hơn cả QT đòi) + khai lệch trình tự so QT | dời site gen | lane-1 F-16 + lane-2 §1-b.13 |
| 11 | Authz Contract | ✅ **RESOLVED — anh chốt O-A @S160: ĐỂ MỞ, không siết. W8 GỠ.** Rủi ro disclose §4.7 | (anh đổi ý lúc nào cũng kịp — bật lại W8 = 1-2 file) | owner @S160 |
| 12 | Grant quyền KHKK (W1) | 🔴 **UPGRADE-if-exists** (`SeedProcurementMasterAccessAsync:2367-2374` khuôn), chạy SAU revoke — **KHÔNG insert** (row `KeHoachKyKet`/`Khkk_*` seed CanRead-only sẵn ⇒ insert = skip-existing no-op ⇒ 403 giết dry-run, review F-S1/F-O3/F-O4). Cấp CanCreate/CanUpdate cho **13/13 role** (đúng O-A "hiển thị hết") + Delete cho Drafter/Admin | siết nhóm hẹp hơn nếu muốn | review 4-lane hội tụ |
| 13 | b.14-16 (soạn/góp ý/đàm phán HĐ) | Nháp HĐ + comment thread + update-draft trong W5/W6 — KHÔNG wave riêng | thêm Bước workflow admin-config (0 code) nếu muốn trạm thật | lane-1 F-19 + review F-06 |

## 6. GATE 5-ANCHOR (bắt buộc TRƯỚC MỖI WAVE — HANDOFF, reviewer đề @S157, gotcha-proof)

Trước thi công wave: mở **5 anchor** của wave đó (danh sách trong từng spec-wave §③) — **≥1 trượt ⇒ chấm lại TOÀN BỘ anchor wave đó** rồi mới code. Anchor per-wave lấy từ lane-2 §7 (đã re-verify 07-29) + spec cũ.

## 7. KHUÔN ACCEPTANCE + CICD (mọi wave)

- Test: `dotnet test SolutionErp.slnx` PASS 0 fail · **baseline = số test đo NGAY TRƯỚC wave bằng chính lệnh đó** (KHÔNG literal "562" — nó tự lão-hoá; review F-B8) · số-sau ≥ baseline + N · **tên test pin cụ thể theo KHUÔN REPO** (anh chốt @S160: English-predicate + danh-từ-VN-không-dấu, vd `ApproveV2_LastLevel_TransitionsToDaPhatHanh` — KHÔNG vị-ngữ-thuần-Việt; census repo 0/460 tên kiểu cũ, review F-B7).
- FE: `npm run build` ×2 app PASS; mirror SHA khi cookie-cutter.
- Deploy: cicd 3-chân-kiềng — CI run PASS · bundle byte-marker (#77, control ÂM phải ĐỘC QUYỀN — bài đợt-5) · data prod verify **bằng curl API** (SSH-SQL client chết S134/S148; sqlcmd chỉ để local/Dev).
- Menu/permission: CHỈ qua seeder (gotcha #84) — mọi thay đổi grant đi cùng commit wave.

## 8. PIPELINE CÒN LẠI (đúng lệnh anh)

1. ✅ 2 `/fable-real invest` (xong, verified) → 2 ✅ Plan cha + specs (file này + 7 spec-wave) → 3 ⏳ **`/fable-clone reviewer`** (ensemble N-lane chấm bộ plan+spec) → 4 ⏳ **`/fable-real reviewer`** chốt cuối → 5 ⏳ **hmw Opus-5-MAX**: mỗi wave = 1 đợt hmw, args con-trỏ vào ĐÚNG spec-wave file (B6 args-mỏng), worker đọc spec + thi công + tự chạy checklist §③; lead + cicd-monitor verify 3-chân-kiềng sau mỗi wave; **gate 5-anchor chạy TRƯỚC mỗi đợt**.
- Điều KHÔNG tự làm khi chưa có gật riêng: **O-C** (vá `ContractWorkflowService.cs:49` Reject-guard — chạm service HĐ, O-3 còn hiệu lực cho phần SỬA service) · số hoá W4 · option B GĐ4.

<!-- END plan-cha · 8 wave (6 chạy + 2 defer) · 1 mig · 12 default đảo được · pipeline 5 bước -->
