# sub-review-gaps-4 — reviewer ADVERSARIAL verify 4 lỗ (S156 BCH post-CEO flow)

> KHUNG RỖNG ghi @lượt-1 TRƯỚC KHI ĐỌC (chống #53 return-garble). Append dần theo lượt.
> ⚠️ PATH-CONFLICT khai báo: task-body chỉ định file này (`sub-review-gaps-4.md`); RUN-TRACE footer chỉ định `sub-reviewer-3.md`. Áp luật "fix-mới thắng footer-cũ" → ghi vào file này, khai trong return.

## 0. Meta
- Vai: reviewer adversarial (READ-only verdict, KHÔNG sửa code-tree)
- Nguồn claim: `sub-invest-bch-1.md` (investigator)
- Ngân sách: 25 lượt (1-2 khung · 3-18 verify · 19-25 chốt)
- Thang chấm mỗi lỗ: **THẬT** / **DƯƠNG-GIẢ** / **ĐÚNG-NHƯNG-KHÔNG-QUAN-TRỌNG**
- Kỷ luật phép đo: MỌI grep 0-hit phải kèm **chứng-có-răng** (grep chuỗi BIẾT CHẮC có, cùng file/cùng lệnh) trước khi kết luận.

## 1. Trạng thái 4 claim (điền dần)

| # | Claim (tóm) | Verdict | Bằng chứng file:line | Chứng-có-răng |
|---|---|---|---|---|
| L1 | cầu PE→HĐ pin V1 `WorkflowDefinitionId`, không nhận `ApprovalWorkflowId` V2 | **THẬT** (cao) | `CreateContractFromEvaluationFeatures.cs:68-71,108` vs `ContractFeatures.cs:105,109`; hệ quả `ContractWorkflowService.cs:98` | cùng-file có hit `ApprovalWorkflowId` ở `:195,199` ⇒ pattern đúng chính tả, vắng-mặt trong initializer là THẬT |
| L2 | `CeoApprovalThreshold` 0 consumer phía HĐ | **THẬT** (cao, ghost-wire) | field `ApprovalWorkflow.cs:43`; `ApplicableType.Contract=3` `ApprovalWorkflow.cs:57`; `ContractWorkflowService.cs:228-232` nạp `aw` mà không đọc; quyết-định terminal `:366-385` thuần vị-trí; đối-chứng PE `PurchaseEvaluationWorkflowService.cs:894` | grep repo-wide `CeoApprovalThreshold` = 40+ hit; grep cùng-file `ApproveV2Async` = 7 hit ⇒ file greppable, 0-hit là THẬT |
| L3 | SLA V2 hardcode `AddDays(7)`, Step không có SlaDays | **THẬT** (vừa) | `ApprovalWorkflow.cs:70-79` (Step 4 prop, 0 SLA); hardcode `ContractWorkflowService.cs:341,358,390` (trong `ApproveV2Async`) + `:85,182,194` | `SlaDays` CÓ ở `WorkflowDefinition.cs:37` (V1) + `PurchaseEvaluationWorkflowDefinition.cs:32` ⇒ token đúng chính tả |
| L4 | `AttachmentPurpose` không phân biệt ký-nháy/ký-chính | **ĐÚNG-NHƯNG-KHÔNG-QUAN-TRỌNG** (như phát biểu) | `ContractAttachment.cs:5-11` đúng verbatim; nhưng 0 nhánh logic đọc `Purpose`: `ContractAttachmentFeatures.cs:51,80,89` + `ContractsController.cs:78` | grep `Purpose` toàn Backend = 25+ hit (Office/PE/VehicleBooking) ⇒ pattern có răng; riêng nhánh `Purpose ==` = 0 |

## 2. Nhật ký đo (append theo lượt)

### Lượt 1-2 — khung rỗng
- Đã ghi file này trước khi đọc bất kỳ file nguồn nào. Chưa đo gì.

### Lượt 3-10 — đo

**L1.** Cầu PE→HĐ chỉ truy V1: `CreateContractFromEvaluationFeatures.cs:68-71` query `db.WorkflowDefinitions.Where(w => w.ContractType == request.ContractType && w.IsActive)`, rồi `:108` gán `WorkflowDefinitionId = activeWfId`. KHÔNG có dòng nào gán `ApprovalWorkflowId`. Nhánh tạo-HĐ-thường thì gán CẢ HAI: `ContractFeatures.cs:105` (`WorkflowDefinitionId = activeWfId`) + `:109` (`ApprovalWorkflowId = request.ApprovalWorkflowId`, comment ghi rõ "[Plan B S29 2026-05-22 Chunk E1] Pin V2 workflow nếu Drafter pick"). Cổng rẽ nhánh: `ContractWorkflowService.cs:98` `if (contract.ApprovalWorkflowId is Guid awId)` → `ApproveV2Async`, else rơi V1 legacy `:108-149`.

Hệ quả ĐO ĐƯỢC của việc rơi legacy (không phải suy đoán): (a) `skipToFinal` ném cứng `ConflictException` — `ContractWorkflowService.cs:104-106`; (b) mất Section 5 ý-kiến-cấp-duyệt V2 — `ContractDtos.cs:51-53` ghi "nếu null → V1 legacy KHÔNG Section 5 V2"; (c) mọi cờ per-slot V2 (F2/F5/F6) vô hiệu vì chỉ đọc trong `ApproveV2Async`.

Hạ-nhiệt trung thực (chống thổi phồng): HĐ sinh từ phiếu vẫn DUYỆT ĐƯỢC vì V1 `WorkflowDefinition` có seed thật (`DbInitializer.cs:721,754`) ⇒ `activeWfId` thường non-null. Đây là "rơi nhánh cũ", KHÔNG phải "hỏng". Rủi ro có điều kiện: nếu 1 ContractType không còn V1 def IsActive thì `activeWfId=null` → tới lượt duyệt mới ném `ConflictException` "HĐ chưa pin workflow definition" (`ContractWorkflowService.cs:115-116`) — tức lỗi hiện MUỘN ở bước duyệt chứ không chặn lúc tạo.

**L2.** Vòng lặp "đặt được → lưu → hiện lại → không ai đọc" khép kín, chứng từng mắt xích:
- Đặt được: input "Ngưỡng giá trị gói CEO (đ)" render VÔ ĐIỀU KIỆN `ApprovalWorkflowsV2Page.tsx:907-912` — không có bọc `applicableType === 1|2` (mọi hit `applicableType` trong file là routing/label/payload: `:262,268,417,733,761`).
- Nhận: validator chỉ chặn `< 0` — `ApprovalWorkflowV2AdminFeatures.cs:277-278`; KHÔNG gate theo `ApplicableType`.
- Lưu: `ApprovalWorkflowV2AdminFeatures.cs:352` passthrough.
- Hiện lại: badge "Ngưỡng CEO: … đ" `ApprovalWorkflowsV2Page.tsx:474-479` ⇒ admin nhìn thấy xác nhận một luật KHÔNG tồn tại.
- Không đọc: `ContractWorkflowService.ApproveV2Async:228-232` nạp NGUYÊN entity `aw` (Include Steps→Levels) nhưng không hề chạm `CeoApprovalThreshold`; quyết định kết-thúc `:366-385` thuần vị-trí (`nextIdx >= steps.Count`).

**Leo thang ngoài phát biểu của invest:** cờ F5 `AllowApproverFinalize` ("Duyệt là kết thúc, không trình CEO") CŨNG vắng trong `ContractWorkflowService.cs` — grep 0 hit, trong khi `AllowApproverSkipToFinal` có 3 hit cùng file (`:210,319,322`) ⇒ chứng grep có răng. Vậy phía HĐ thiếu CẢ HAI cơ chế quyết-định-theo-giá-trị mà PE đã có. Lỗ rộng hơn 1 cột.

**L3.** Schema: `ApprovalWorkflowStep` (`ApprovalWorkflow.cs:70-79`) đúng 4 prop `ApprovalWorkflowId / Order / Name / DepartmentId` — không SLA; `ApprovalWorkflowLevel:95-171` cũng không. Hardcode trong `ApproveV2Async`: `:341` (skipToFinal), `:358` (sang Cấp kế), `:390` (sang Bước kế) — cả 3 đều `AddDays(7)`.

Hai đính chính so với phát biểu của invest:
1. **Đây là THỤT LÙI so với V1, không phải "chưa làm"**: V1 đọc SLA theo cấu hình (`WorkflowPolicy.cs:199` `sla[s.Phase] = s.SlaDays is int d ? …`, nguồn `WorkflowDefinition.cs:37`), và chính lúc tạo HĐ vẫn xài `workflow.GetPhaseSla(...)` (`ContractFeatures.cs:110`).
2. **Không phải lỗ riêng của HĐ — là lỗ toàn schema V2**: PE V2 hardcode y hệt (`PurchaseEvaluationWorkflowService.cs:268,534,838,935,960,1163`).

SLA có răng thật (không phải số trang trí): `SlaExpiryJob.cs:77` lọc `SlaDeadline < now` bắn thông báo quá hạn, `:140-153` cảnh báo sắp hết hạn. Nên hệ quả = đồng hồ quá-hạn chạy sai nhịp cho MỌI bước V2, chứ không chặn luồng.

**L4.** Enum đúng verbatim: `DraftExport=1 / ScannedSigned=2 / SealedCopy=3 / Other=99` (`ContractAttachment.cs:5-11`). Nhưng phát biểu bị đóng khung SAI TẦNG: `Purpose` hiện là **nhãn không ai thi hành** — validator chỉ `IsInEnum()` (`ContractAttachmentFeatures.cs:51`), lưu (`:80`), trả ra DTO (`:89`), controller mặc định `Other` (`ContractsController.cs:78`); không có nhánh `Purpose ==` nào trong Backend, cũng không có ràng buộc "ScannedSigned chỉ ở phase DangInKy". PE còn chép nguyên nhận định này thành comment: `PurchaseEvaluationWorkflowService.cs:190` "FE KHÔNG check Purpose enum → BE cũng không".

⇒ Thêm 2 giá trị enum ký-nháy/ký-chính sẽ KHÔNG tạo ra năng lực nào, vì không có mã nào đọc. Lỗ thật nằm chỗ khác và lớn hơn: đính-kèm không có cổng thi-hành theo mục-đích. Đồng thời có lối thoát rẻ sẵn có (`Note` — `ContractAttachment.cs:21`, và `Other=99`).

## 3. Kết luận

- **2 lỗ đáng làm:** L1 (cầu PE→HĐ bỏ pin V2) và L2 (ngưỡng CEO là ghost-wire phía HĐ, kèm F5 cũng vắng).
- **1 lỗ thật nhưng phải đổi khung:** L3 — là thụt-lùi-so-V1 và là lỗ toàn-V2 (cả PE), không riêng HĐ; hệ quả ở tầng thông-báo.
- **1 lỗ đúng-chữ nhưng sai-tầng:** L4 — sửa enum là vô nghĩa nếu không dựng cổng đọc `Purpose`.
- **0 DƯƠNG-GIẢ.** Invest không bịa lỗ nào; điểm yếu của invest là ĐÓNG KHUNG (L3 hẹp thành lỗi HĐ, L4 quy về schema thay vì thi-hành), không phải bịa dữ kiện.

END · TOTAL=4 CLAIM
