# SPEC WAVE-6 — Đường ống HĐ V2: view + inbox + vai-trình (GĐ3 sống)

> Nguồn: lane-2 §1 (bảng SẴN/GAP/PARTIAL re-verify 07-29) + §6-W6. Điều kiện tiên quyết VẬN HÀNH của GĐ3 — không có nó, HĐ pin V2 xong vẫn "vô hình" với approver ("cơ chế đúng, đường đi không có"). Build độc lập W1-W3 (0 đụng file KHKK) — chạy sớm được.

## ① TÍNH NĂNG / ĐỀ BÀI

3 lỗ đường ống làm HĐ V2 không vận hành được (đo 07-29):
1. **Approver V2 KHÔNG XEM được HĐ `ChoDuyet`** — view-guard `GetEligiblePhases` (`ContractFeatures.cs:334-351`) 0 entry ChoDuyet cho approver (`:488-489` ném Forbidden).
2. **HĐ ChoDuyet KHÔNG vào inbox** — `PhaseActorRoles` (`:421-430`) 100% legacy phase.
3. **PMH role `Procurement` bị 403 khi trình** — guard trình đòi `Drafter∨DeptManager` (`ContractWorkflowService.cs:73-78`).
Wave này = 0 UI mới, 0 mig; sau deploy: approver thấy + mở + duyệt được HĐ đang chờ mình.

## ② CÁCH IMPLEMENT

1. **Gate 5-anchor** (§③-A).
2. **Test-before** 3 test (§③-B) RED.
3. 🔴 **View — ĐÚNG CÁCH per-HĐ, KHÔNG role-blanket (review F-S3/F-C2/F-01):** `GetEligiblePhases(userRoles)` (`ContractFeatures.cs:334-350`) chữ ký **chỉ nhận role** ⇒ nếu chỉ nhét `ChoDuyet`/`DaPhatHanh` vào tập role thì mở toang cho MỌI user mang role đó (kể cả HĐ xóa mềm ở `:381`). **Fix:** thêm nhánh V2 **membership-theo-HĐ** ở CẢ 3 call-site của `GetEligiblePhases` (`:301` List · `:381` ListDeleted · `:486` Detail — review F-S3 chỉ ra gate cũ thiếu 2 site) — user thấy HĐ `ChoDuyet` **nếu là `ApproverUserId` của Level trong workflow pin CỦA CHÍNH HĐ đó**, và HĐ `DaPhatHanh` **nếu thuộc workflow HĐ đó HOẶC mang vai bản-cứng** (Director/AuthorizedSigner/HrAdmin/CostControl — actor b.19/20/21, cho GĐ4/W7). Admin giữ thấy-hết. KHÔNG mở toang: user ngoài cả hai điều kiện → 403 y cũ. *(Đây là điều kiện W7 phụ thuộc — F-C2.)*
4. **Inbox**: mirror khuôn PE `ResolveV2InboxIdsAsync` (precompute id-set các HĐ mà con-trỏ Step/Level hiện tại chứa `ApproverUserId==actor`) — GHÉP vào inbox query hiện có thay vì chỉ `PhaseActorRoles` legacy. Giữ nhánh legacy cho HĐ V1 (nếu prod xuất hiện lại data V1). 🔴 **Nhánh ADMIN (F-B13, DR-4 chốt @S161):** admin hiện lấy `eligiblePhases = PhaseActorRoles.Keys` (`ContractFeatures.cs:440-441`) mà dict `:421-430` KHÔNG có `ChoDuyet` ⇒ ghép v2InboxIds xong admin VẪN mù HĐ ChoDuyet. Mirror TRỌN khuôn PE `PurchaseEvaluationFeatures.cs:731-748`: admin dùng danh-sách phase liệt TƯỜNG MINH **+ `ChoDuyet`** (admin thấy mọi HĐ chờ duyệt qua nhánh PHASE; `v2InboxIds` để rỗng cho admin đúng như PE `:746-748`).
5. **Guard trình** (`ContractWorkflowService.cs:73-78`): nới thành `Drafter ∨ DeptManager ∨ CreatedBy==actor ∨ Procurement` (dòng tối thiểu đóng lớp 403-PMH; giữ 403 cho role ngoài).
6. (Nếu DTO cần) thêm `currentApproval` info vào detail DTO cho FE panel — chỉ khi FE thiếu dữ liệu hiển thị.
7. Build + test GREEN + commit `[CLAUDE] Contract: W6 duong ong V2 view+inbox+trinh`. cicd verify.

## ③ CHECKLIST

**A. Gate 5-anchor:** `ContractFeatures.cs:334-351` · `:421-430` · `:488-489` · `ContractWorkflowService.cs:73-78` · `ContractsController.cs:38-40` (khu inbox/list endpoint).
**B. Acceptance (lane-2 §7-W6):**
- [ ] 5 test ĐÚNG TÊN (khuôn repo) trong `ContractV2PipelineTests`: `Inbox_PendingLevelApprover_SeesChoDuyetContract` · `Inbox_OtherLevel_DoesNotSeeContract` (ca ÂM) · `View_V2Approver_NotDrafter_CanSeeChoDuyetContract` · `List_HardCopyActor_NotAdmin_SeesDaPhatHanhContract` (🔴 review F-01: LIST test cho actor bản-cứng — không wave nào đo LIST trước) · `View_UserOutsideWorkflow_WithUnrelatedRole_Gets403` (🔴 review F-S4: ca ÂM **pin ROLE cụ thể** như CostControl/Procurement, KHÔNG chọn HrAdmin cho pass dễ).
- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 5.
- [ ] Prod ③: login approver-V2 KHÔNG-admin → `GET /api/contracts/inbox` ≥1 HĐ ChoDuyet mà mình là Level hiện tại + `GET /{id}` 200; login **admin** → `/api/contracts/inbox` ≥1 HĐ ChoDuyet (🔴 F-B13 — trước W6: admin 0 dòng vì `PhaseActorRoles.Keys` không có ChoDuyet); login actor bản-cứng non-admin (vd Director) → `GET /api/contracts?type=1&phase=9` ≥1 dòng (trước W6: 0 dòng + 403).
- [ ] Ca ÂM giữ (pin role): user role KHÔNG liên quan + không thuộc workflow HĐ nào → `GET /{id}` vẫn 403 (chứng KHÔNG mở toang qua role-blanket).
- [ ] 🔴 Kiểm hở xóa-mềm: HĐ `ChoDuyet` đã soft-delete → user V2 KHÔNG thấy ở `:381` ListDeleted (fix membership không được rò sang màn Đã-xóa).
- [ ] CI run PASS; 0 mig (diff `*Migrations*` = 0 — khuôn M2 đợt-5).
**C. Rủi ro:** [ ] đổi view-guard làm hở HĐ cho MỌI user (thiếu điều kiện per-HĐ) — ca ÂM pin-role + kiểm-xóa-mềm canh · [ ] inbox V2 quên lọc `IsDeleted` · [ ] SỬA CHỈ 1/3 call-site `GetEligiblePhases` (F-S3: gate cũ thiếu `:301`+`:381`) — grep 3 site trước khi đóng · [ ] đụng nhánh Reject-trước-guard (`:49-66`) = O-A anh chốt ĐỂ MỞ, ĐỪNG "tiện tay sửa".
