# W4 — guard 4-cửa pin/re-pin quy trình HĐ (BE, 0-migration)

> vai: implementer-backend · run `2026-08-10-S187-duyet-hd-tu-khkk` · đề bài: lead W4
> đáp án owner neo vào: (86) KHOÁ CỨNG không đổi chéo loại (mirror KHKK/Duyệt NCC) · (80) trục 7-LOẠI · (81) PA-A

## 0. Đo TRƯỚC khi sửa (không nhận tiền-đề từ đề bài)

### 0.1 Nhà 7 mã ĐÃ LAND (dùng, cấm gõ lại chuỗi)

`src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs` — có trên đĩa (untracked, W3 đang land),
`CodePrefix = "HD-"` · `Members` 7 bộ ba (Type, Code, ShortName) · `CodeFor(type)` · `KeyToken(code)`.
Consumer hiện có: `DbInitializer.cs:756/:782` (seed 7 workflow) + `MenuKeys.cs:155-156` (7 key Designer) + test W3.
⇒ W4 là consumer thứ 3, gọi `CodeFor`, KHÔNG khai lại mã.

### 0.2 Dev DB — item 3 của đề bài ("có HĐ nào pin workflow ngoài họ không?")

```
sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -E
SELECT COUNT(*) , SUM(CASE WHEN ApprovalWorkflowId IS NOT NULL THEN 1 ELSE 0 END) FROM Contracts;
  → 7 | 0          ← 7 HĐ tồn tại (control dương: bảng KHÔNG rỗng) · 0 HĐ pin V2
SELECT Code,Version,ApplicableType,IsActive,IsUserSelectable FROM ApprovalWorkflows WHERE ApplicableType=3;
  → QT-HD-V2-001 | 1 | 3 | 1 | 1     ← DUY NHẤT, và mã NGOÀI họ HD-*
SELECT ApplicableType, COUNT(*) ... GROUP BY ApplicableType;
  → 3:1 · 4:1 · 5:1 · 6:1 · 7:1 · 9:1 · 10:8   ← control dương: type-10 có 8 (họ KHKK đã seed) ⇒ thước sống
```

**Kết luận đo:** Dev **0 HĐ pin quy trình ngoài họ** (vì 0 HĐ pin V2 nào cả — 7/7 đều `NULL`).
Quy trình type-3 DUY NHẤT đang sống trên Dev lại là `QT-HD-V2-001` (mã ngoài họ) — W3 seeder `HD-*` chưa chạy trên Dev
(code có trên đĩa, chưa restart API). ⇒ Ngay hôm nay, người dùng mở màn tạo HĐ chỉ thấy đúng 1 lựa chọn: `QT-HD-V2-001`.

### 0.3 🔴 ĐỌC ASSERT TRƯỚC KHI CHỌN HÌNH RÀO (bài S187 W1-GREEN)

Đề bài viết: *"`Code` phải == `CodeFor(contractType)`; workflow mã NGOÀI họ/khác loại ⇒ lỗi"* (dạng **bằng-nhau-tuyệt-đối**).
Đo trên đĩa thì hình đó **ĐỎ HÀNG LOẠT** ở tests/** — mà tests/** tôi **CẤM sửa**:

| Test (không được sửa) | Pin mã gì | Hình bằng-nhau-tuyệt-đối cho kết quả |
|---|---|---|
| `CreateContractFromSigningPlanTests` T1-T11 (`:186` `QT-HD-V2-K7-{tag}`, `TestType=HopDongThauPhu` ⇒ họ đòi `HD-TP`) | ngoài họ | **ĐỎ** cả 11 ca, kể cả ĐỐI CHỨNG DƯƠNG `:496` |
| T8 `:543` — HĐ do cầu đẻ đem TRÌNH qua `TransitionAsync` | ngoài họ | **ĐỎ** nếu cửa TRÌNH cũng bằng-nhau-tuyệt-đối |
| `ContractV2PipelineTests` `:609` `QT-CT-T7` + `:624` submit | ngoài họ | **ĐỎ** |
| `ContractWorkflowServiceApproveV2Tests` `QT-CT-TERMINAL` / `QT-CT-F2` | ngoài họ | ĐỎ nếu siết cả đường duyệt |

Và chính chữ owner (86) là **"mirror KHKK/Duyệt NCC"** — khuôn KHKK
(`ContractSigningPlanWorkflowGuards.cs:56-64`) khai TƯỜNG MINH semantics: *mã TRONG họ phải khớp · mã NGOÀI họ CHO QUA
có chủ đích ("không mang thông tin nhóm ⇒ không có gì để đối chiếu, chặn nó là bịa ràng buộc")*.

⇒ **Hình đã chọn = KHOÁ THEO KHÔNG-GIAN-TÊN `HD-`** (chặt hơn KHKK một nấc, vẫn khớp disk + khớp chữ owner):

```
code bắt đầu bằng "HD-"  ⇒ PHẢI == CodeFor(type)   (sai ⇒ 409 "gắn chéo loại")
code NGOÀI "HD-"         ⇒ CHO QUA                 (QT-HD-V2-001, QT-CT-*, quy trình admin tự đặt)
```

Nó bắt ĐÚNG cái owner sợ (pin `HD-MB` lên HĐ Thầu phụ = đổi chéo loại) **và** bắt thêm ca gõ nhầm trong chính
không-gian-tên họ (`HD-TPX`, `HD-TP-V2` — sai thì nổ, không im lặng), mà **0 test ĐỎ** (0 mã test nào bắt đầu bằng `HD-`;
đã grep toàn tests/: các mã là `QT-*`, `K7-*`, `BTBM`, `FLOCK01`).
Lệch đề bài đúng 1 nấc này khai ở §4 để lead bác nếu muốn siết tiếp (siết = phải sửa tests/, việc của test-specialist).

### 0.4 Loại exception — đề bài nói "ValidationException", đĩa nói KHÔNG

- `Common/Exceptions/ApplicationException.cs:10-25`: `ValidationException` **KHÔNG có ctor nhận message**
  (message cố định "Có lỗi trong dữ liệu gửi lên.", chỉ mang `Errors` dict) ⇒ "ValidationException tiếng Việt rõ ràng"
  không dựng được nếu không sửa file exception dùng chung.
- `tests/.../CreateContractCommandApplicableTypeTests.cs:88-89` assert **`ConflictException`** + `"*ApplicableType=Contract*"`,
  và chú thích `:22-24` nói thẳng "spec mention ValidationException — thực tế throw ConflictException".
- Khuôn KHKK `EnsureWorkflowGroupMatchAsync` cũng `ConflictException`.
⇒ **Dùng `ConflictException` (409) + message tiếng Việt rõ ràng**; `NotFoundException` giữ cho nhánh id-không-tồn-tại
(T7-a `:470` assert đúng loại này).

### 0.5 Ràng buộc chữ MESSAGE (2 test assert 2 chuỗi KHÁC NHAU cho CÙNG một vế)

Vế "sai ApplicableType" nay do 1 nhà chung phát ngôn, mà hai test đòi hai chuỗi:
- `CreateContractCommandApplicableTypeTests.cs:89` → `*ApplicableType=Contract*`
- `CreateContractFromSigningPlanTests.cs:476` (T7-b) → `*không khớp với Hợp đồng*`
⇒ message hợp nhất PHẢI chứa CẢ HAI:
`Quy trình '{Code}' áp dụng cho {ApplicableType}, không khớp với Hợp đồng (cần ApplicableType=Contract).`
3 message còn lại giữ nguyên văn bản bridge (`*ngừng hoạt động*` `:482` · `*chưa được ghim*` `:488`).

</content>
