W10: ĐANG CHẠY — file mở lúc bắt đầu, APPEND từng bước (chống #53)

# sub-w10-dryrun — DRY-RUN toàn trình GĐ3 "Duyệt HĐ từ KHKK" trên DEV (S187/S188, 2026-08-10)

> Khuôn mượn: `runs/2026-07-31-S164-4gd-khkk-fanout/sub-k8-dryrun.md` (K8 @S168) — B0 pre-flight fail-closed → sổ ID → nhật ký từng bước → sổ seq cháy.
> 🔴 Khác K8: K8 chạy trên **PROD**; W10 này chạy **LocalDB Dev + API local (:5443)** — KHÔNG đụng prod (mandate).
> Kịch bản 2 nhánh: A = `HD-TP` (Thầu phụ) auto-pin · B = `HD-DV` (Dịch vụ) thử sai-họ 409 trước rồi chạy trọn.

## B0 — PRE-FLIGHT (đang đo)

**Thời điểm bắt đầu đo:** 2026-08-10 ~19:55 (giờ máy). Mọi số dưới đây kèm lệnh tái-lập.

| # | Cửa | Lệnh | Kết quả |
|---|---|---|---|
| B0.1 | API sống | `curl -s -o /dev/null -w "%{http_code}" http://localhost:5443/api/contracts` | **401** (auth-required = API sống); `/swagger/index.html` → **200** |
| B0.2 | Tiến trình API | `Get-CimInstance Win32_Process -Filter "Name='dotnet.exe'"` | PID **62260** `dotnet run --project src/Backend/SolutionErp.Api --urls http://localhost:5443`, CreationDate **19:49:02** — API **ĐÃ SỐNG SẴN từ lane trước**, KHÔNG cần relaunch |
| B0.3 | Mã đang chạy = HEAD? | `ls --time-style=full-iso .../bin/Debug/net10.0/SolutionErp.{Application,Infrastructure}.dll` | **19:49:09.957** / **19:49:17.177** — DLL ghi *sau* khi PID 62260 sinh (19:49:02) ⇒ chính nó build+load. `git log` HEAD=`178b746c` (chỉ `wal:`), 2 commit mã cuối `b275893c` (W5+W6FE) + `ad9a21ed` (W4+W6BE) **đã land trước 19:49** ⇒ API chạy **đúng mã W4/W5/W6**. `git status` **sạch**. |
| B0.4 | DB Dev | `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "SELECT TOP 3 MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC"` | top = `20260806070242_AddMaHangMucToContractSigningPlanLines` (**Mig 72**) — Dev **đã** apply 72 (đính chính memory S182 "Dev dừng ở 71": lane API boot đã tự Migrate) |
| B0.5 | 7 workflow HĐ | `SELECT Code,Version,ApplicableType,IsActive,IsUserSelectable FROM ApprovalWorkflows WHERE ApplicableType IN (3,10)` | type-3: **8** dòng = 7 họ `HD-TP/GK/MB/NCC/DV/NT-DV/NT-NCC` (v1, IsActive=1, IsUserSelectable=1) **+ `QT-HD-V2-001`** (workflow UAT cũ, cũng type-3 active) · type-10: **8** `KHKK-N1..N8` |
| B0.6 | Roster HD-TP / HD-DV | join `ApprovalWorkflows×Steps×Levels×Users` | **giống hệt nhau, 3 bước × 5 trạm**: B1 *Phòng Cung ứng* Cấp1 = OR-of-3 `long.chau`/`phuong.nguyen`/`duy.nguyen`, Cấp2 = `tra.bui` · B2 *P. Kiểm soát Chi phí* Cấp1 = OR-of-3 `binh.le`/`nguyen.ho`/`luu.tran`, Cấp2 = `chuong.phan` · B3 *Ban Giám đốc* Cấp1 = `truong.nguyen` |
| B0.7 | Dữ liệu nền | `SELECT COUNT(*) FROM Contracts / Projects / Suppliers` | Contracts **7** · Projects **69** · Suppliers **24** |
| B0.8 | KHKK sẵn có | `SELECT Phase,COUNT(*) FROM ContractSigningPlans WHERE IsDeleted=0 GROUP BY Phase` + `SELECT COUNT(*) FROM ContractSigningPlanLines` | **0 phiếu · 0 line** ⇒ nhánh A **PHẢI tự dựng KHKK** (dùng đường shortcut đã khai trong brief) |
| B0.9 | Đăng nhập | `POST /api/auth/login` | `admin@solutions.com.vn` / `Admin@123456` → **200**. 5 approver với `User@1234567` → **401**; đọc mã: `DbInitializer.cs:1868` đổi 11→12 ký tự @S58 nhưng **const chỉ áp lúc CREATE** ⇒ user Dev cũ giữ pass cũ `User@123456` (`git show 59981636`). |

🔴 **Ràng buộc vận-hành phát hiện ở B0:** `Program.cs:105` `RateLimit:AuthLoginPerMinute` **mặc định 5/phút/IP** cho policy `auth-login` ⇒ kịch bản cần ~7 danh tính **BẮT BUỘC** cache token tái dùng, không login lại mỗi bước (đã đụng **429** đúng 1 lần lúc dò mật khẩu).

## B1 — DỰNG PHIẾU KHKK (🔴 **SHORTCUT — khai rõ**)

🔴 **Đây KHÔNG phải dry-run của module KHKK.** Phiếu được dựng bằng `INSERT` thẳng SQL,
**bỏ qua trọn** đường CRUD → gán hạng mục → submit → duyệt 8-nhóm của GĐ2. Lý do: W10 đo
**GĐ3 (Duyệt HĐ)**; GĐ2 đã có dry-run riêng (K8 @S168). Hệ quả phải nhớ khi đọc kết quả:
mọi rào **phía GĐ2** (submit-guard bắt line chưa gán hạng mục, `EnsureWorkflowGroupMatchAsync`
nhóm N1-N8, choke-point `ApprovedAmount ??=`) **KHÔNG được đo ở đây** — chúng bị đi vòng.

**Lệnh:** `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -I -i b1-seed-khkk.sql`
🔴 **Bẫy đo:** thiếu cờ `-I` ⇒ `Msg 1934 ... 'QUOTED_IDENTIFIER'` — bảng `ContractSigningPlanLines`
có **filtered index** (`IX_..._ContractSigningPlanId_SupplierId_CatalogEntryId` UNIQUE
`WHERE [IsDeleted]=0`), mà `sqlcmd -i` mặc định `QUOTED_IDENTIFIER OFF` ⇒ **mọi** DML trên bảng
có filtered index đều chết. (`sqlcmd -Q` inline thì mặc định ON — nên lỗi chỉ hiện ở đường `-i`.)
Bẫy phụ: `sqlcmd < file` (stdin) chạy **im lặng 0 dòng, 0 lỗi** — đừng đọc "không lỗi" thành "đã chạy".

**Dữ liệu dựng** (mọi id cố định để tra lại; vùng test `KHKK/2026/9xx` + marker `ZZTEST`):

| Vật | Id | Giá trị |
|---|---|---|
| Phiếu KHKK | `9B100000-…-000000000901` | `MaKeHoach=KHKK/2026/901` · `Phase=3` (DaDuyet) · `ApprovalGroup=1` · PE=`C991F820…` (`[DEMO]-A-003`) · Project=`B3BE2FE2…` (`APVN01`) · Drafter=admin |
| Line A | `9B100000-…-00000000A001` | Supplier `6D0EFB1E…` (**NCC-DIEN**) · `ApprovedAmount=1.500.000.000` · `ContractId=NULL` |
| Line B | `9B100000-…-00000000B001` | Supplier `F52B48C7…` (**DV-BAOVE**) · `ApprovedAmount=500.000.000` · `ContractId=NULL` |

**Vì sao 2 NCC khác nhau, không phải 2 dòng cùng NCC:** unique filtered index 3-cột
`(PlanId, SupplierId, CatalogEntryId)` — 2 dòng cùng NCC mà `CatalogEntryId` đều `NULL` sẽ **đụng
index** (SQL Server coi 2 `NULL` là **bằng nhau** trong unique index). Tách NCC là đường rẻ nhất,
đồng thời cho mỗi nhánh A/B một dòng riêng (rào (5) `line.ContractId is not null` = mỗi dòng chỉ
bridge được 1 lần).

**Verify (SELECT lại sau INSERT):** 1 phiếu Phase=3 + 2 line `ApprovedAmount` NOT NULL,
`ContractId` cả 2 = `NULL` ⇒ đủ điều kiện qua rào (1)(4)(5) của `CreateContractFromSigningPlanCommandHandler`.

**Ground-truth đọc từ mã trước khi chạy B2** (khỏi đoán):
- `ContractType`: `ThauPhu=1` · `GiaoKhoan=2` · `NCC=3` · **`DichVu=4`** · `MuaBan=5` · `NT-NCC=6` · `NT-DV=7` (`Domain/Contracts/ContractType.cs`) ⇒ brief đoán đúng cả 2.
- `ContractSigningPlanPhase.DaDuyet = 3` (`Domain/ContractSigningPlans/ContractSigningPlanPhase.cs:12`).
- `ContractPhase`: Nháp=`DangSoanThao(2)` → **`ChoDuyet(10)`** → terminal **`DaPhatHanh(9)`**; `TraLai=98` · `TuChoi=99`.
- TRÌNH/duyệt HĐ = **cùng một cửa** `POST /api/contracts/{id}/transitions`
  body `{targetPhase, decision, comment}` (`ContractsController.cs:64-69`), `ApprovalDecision.Approve=1`.


## B2-B5 — KẾT QUẢ (lead tự chạy sau 4 đời agent chết non; driver = scratchpad/w10.py, 4 stage)

- **B2 Nhánh A (HD-TP):** bridge KHÔNG gửi workflowId → auto-pin đúng `HD-TP` (61C2260B) · detail có `source.maKeHoach=KHKK/2026/901` + `workflowSteps` 3 bước · submit 204 · **sai-trạm binh.le → 403** · duyệt đúng thứ tự long.chau→tra.bui→binh.le→chuong.phan→truong.nguyen (204 ×5) · re-approve sau khi qua trạm → 403 đích danh "Bước 3 (Ban Giám đốc) — Cấp 1: bạn không có trong danh sách NV duyệt" · **phase cuối = 9 DaPhatHanh** ✅. HĐ A: APVN01/HDTP/SOL&NCC-DIEN/01|9
- **B3 Nhánh B (HD-DV):** gửi workflowId HD-TP cho loại Dịch vụ → **409** "Quy trình 'HD-TP' là quy trình duyệt của loại Hợp đồng KHÁC — HĐ loại 'Dịch vụ'…" ✅ · bridge auto → **201** `e348f5d3` mã **APVN01/HĐDV/SOL&DV-BAOVE/01** · submit 204 (đang ChoDuyet trạm 1)
- **B4 F3 inbox:** long.chau (Cấp-1 đang chờ) **THẤY** phiếu B = True · chuong.phan (ngoài cấp) = **False** ✅ — chốt verify-only của stage C đứng.
- **B5 Sổ ID/seq cháy (Dev):** KHKK `KHKK/2026/901` (9B100000-…-0901, SHORTCUT-INSERT bỏ qua GĐ2) · Line A→HĐ 7D047E08 · Line B→HĐ e348f5d3 · 2 mã HĐ ZZTEST-W10 đã CHÁY SEQ codegen Dev (ai đọc Dev sau đừng tưởng data thật). API PID 62260 ĐỂ NGUYÊN (khai). Ghi chú khác mã dự kiến: sai-trạm trả **403** (Forbid) không phải 409 — hành-vi CHẶN đúng, mã do W6-BE chọn.
- Vế KHÔNG đo (khai): rào GĐ2 (submit-guard/nhóm N1-N8/choke-point) — đi vòng bằng shortcut B1, đã có K8 @S168 phủ.

**W10: PASS — 5/5 bước.**

== HẾT ==
