**HEADLINE:** GO-WITH-ADJUSTMENTS — hướng đúng, nhưng guard EDGE-5 theo spec BỎ SÓT đường admin-override (SVC:290) nên chưa "cứng" thật, và fix A1 MỞ một đường CEO-skip MỚI cần anh Kiệt chốt trước khi fan-out + deploy prod.

## Lăng-kính SECURITY / GOVERNANCE — kết quả kiểm

Tôi đã đọc source thật (không tin framing của spec/investigation). Hai vấn đề trọng tâm được xác minh dưới đây, kèm 4 mục phụ.

---

### 1. EDGE-5 hard-lock (Decision 3) — CẦN ĐIỀU CHỈNH vị trí guard (must-fix trước implement)

**Lỗ hổng hiện tại đã xác nhận (đúng EDGE-5):** một phiếu `DaDuyet` bị lật ngược được ngay hôm nay. Truy vết `TransitionAsync`: decision=Reject vào reject-branch (SVC:92) → `EnsureCanRejectV2Async` **early-return im lặng tại SVC:321** vì `Phase != ChoDuyet` (bỏ qua actor-scope) → `ApplyReturnModeAsync` mode mặc định Drafter → `evaluation.Phase = TraLai` (SVC:420). Phiếu đã duyệt quay lại Trả lại. Xác nhận.

**Spec đề xuất đặt guard ở: reject-branch (SVC:92) + `EnsureCanRejectV2Async` :321 + handler FEAT:510-539 + controller. Đây là INCOMPLETE.** Nó phủ được đường reject/return, nhưng **bỏ sót đường admin-override tại SVC:290**:

```
289  // Admin manual override (vd test cứng phase)
290  if (isAdmin) { evaluation.Phase = targetPhase; ... return; }
```

Một Admin gửi `decision=Approve`, `targetPhase=<bất kỳ>` trên phiếu `DaDuyet`: KHÔNG vào reject-branch (SVC:92 đòi decision==Reject), KHÔNG vào approve-branch (SVC:268 đòi fromPhase==ChoDuyet) → rơi thẳng xuống SVC:290 → **un-terminal phiếu**. Decision 3 nói "chặn MỌI reject/trả-lại/transition" + "Đã duyệt rồi là cứng" ⇒ phải chặn CẢ Admin. Guard chỉ đặt ở reject-branch KHÔNG đạt yêu cầu này.

**ĐỀ XUẤT CHÍNH XÁC (1 guard duy nhất, đầu method):** chèn ngay sau `var fromPhase = evaluation.Phase;` tại **SVC:53** (trước guard TraLai/TuChoi hiện có ở SVC:57):

```
if (fromPhase is PurchaseEvaluationPhase.DaDuyet or PurchaseEvaluationPhase.TuChoi)
    throw new ConflictException(
        $"Phiếu đã ở trạng thái kết thúc ({fromPhase}) — không thể trả lại, từ chối hay chuyển trạng thái. Đã duyệt/từ chối là cố định.");
```

**Chứng minh ĐỦ KÍN — 5 đường ra của `TransitionAsync` đều nằm SAU SVC:53:**

| Đường | Vị trí | Guard đầu method chặn? |
|---|---|---|
| Reject / 4 return-mode (qua `ApplyReturnModeAsync`, private, caller duy nhất = SVC:118) | SVC:92 | ✅ |
| Drafter submit/resubmit | SVC:150 | ✅ |
| Approve step (ApproveV2/V1) | SVC:268 | ✅ |
| **Admin override** | SVC:290 | ✅ (đây là chỗ spec bỏ sót) |
| Fall-through throw | SVC:300 | ✅ |

**Bài học pattern (ghi diary):** terminal-lock phải gác **FROM-state ở ĐỈNH method**, KHÔNG rải guard per-branch — rải per-branch luôn bỏ sót nhánh admin/fall-through.

**Lưu ý thêm:**
- **KHÔNG sửa semantics `EnsureCanRejectV2Async` :321.** Các early-return của nó phục vụ ca hợp lệ (system/cron reject phase khác). Khi đã có guard đỉnh-method, không cần đụng :321; đụng vào có rủi ro vỡ đường system.
- Controller PurchaseEvaluationsController :130-143 chỉ có `[Authorize]` class-level (đúng pattern gotcha #44), không kiểm phase; handler FEAT:510-539 nạp entity nhưng cũng không kiểm phase ⇒ **service là cổng duy nhất**, nên đặt ở đầu service là single-source đúng. Thêm guard ở handler chỉ là defense-in-depth thừa, KHÔNG bắt buộc.
- **Hệ quả cần anh Kiệt biết:** guard này gỡ luôn "escape hatch" của Admin (SVC:290) để lật một phiếu bị finalize nhầm — sau này chỉ sửa được bằng DB. Đây đúng là ý "cứng", nhưng cần xác nhận chấp nhận.

---

### 2. Decision 2 (không hard-guard self-approve) — RỦI RO CEO-SKIP, phải escalate anh Kiệt

**Xác minh chuỗi:** `DrafterUserId = currentUser.UserId` (FEAT:147) ⇒ creator chính là drafter. Sau A1, pointer đứng tại ô Cấp thấp nhất của creator; creator duyệt qua `ApproveV2Async`, match-approver SVC:728-739 CHO QUA vì creator ∈ `ApproverUserId`.

**Đường CEO-skip MỚI do chính A1 tạo ra:** nếu ô của creator có `AllowApproverFinalize=true`, `ApproveV2Async` **SVC:867** (`if (matchingLevel.AllowApproverFinalize && applyLevelFinalize)`) set `Phase=DaDuyet` → **bỏ qua mọi Bước/Cấp sau, kể cả CEO**. FE `PeWorkflowPanel.tsx:203` gửi `applyLevelFinalize` = giá trị ô-tick khi `approverFinalizeEligible`; creator VẪN thấy nút "✓ Duyệt" (nút Trả lại/Từ chối bị ẩn tại :446, nhưng Duyệt thì không). Mặc định ô-tick = false (opt-in S96/S97) ⇒ cần creator **chủ động tích**, nhưng hệ thống CHO PHÉP.

**Đính chính framing của investigation (giá trị độc lập):** đường này **KHÔNG tồn tại trước A1** với workflow nhiều-bước-có-CEO. Trước A1, `ApplyDrafterBypassOnSubmitAsync` tự advance VƯỢT QUA ô creator (SVC:645-666) mà KHÔNG hề gọi `AllowApproverFinalize` ⇒ CEO không bị bỏ. Ca self-finalize duy nhất tồn-tại-nay là 1-bước-drafter-là-cấp-cuối (SVC:667-680) — ca đó **không có bước CEO nào cả**. Vậy A1 **thực sự MỞ RỘNG bề mặt phơi nhiễm**; câu invest-C "đã tồn tại hôm nay" là IMPRECISE (chỉ đúng cho 1-step-no-CEO).

**`skipToFinal` KHÔNG phải CEO-skip:** nó chỉ nhảy pointer tới Cấp cuối, Phase giữ `ChoDuyet`, người cuối (CEO) VẪN phải ký (SVC:805-846). Chỉ nguy nếu creator ĐỒNG THỜI là approver Cấp cuối — cấu hình bệnh lý, rủi ro thấp.

**Escalate:** lý do Decision 2 ("cấp chỉ 1 người → tạo và duyệt luôn được") nói về việc DUYỆT một cấp một-người, KHÔNG nói về việc trao cho creator quyền tự KẾT THÚC phiếu bỏ CEO. Đây nhiều khả năng NGOÀI Ý. Xem openQuestion 1.

**Nếu anh Kiệt muốn bịt (đề xuất chính xác, tôn trọng Decision 2):** guard hẹp chỉ chặn self-**FINALIZE** (không phải self-approve), đặt tại **SVC:867** và **SVC:805**:
```
// tại SVC:867, thêm điều kiện trước khi finalize:
if (matchingLevel.AllowApproverFinalize && applyLevelFinalize
    && actorUserId == evaluation.DrafterUserId && !isAdmin && !isSystem)
    → soft-warn hoặc throw (tùy anh Kiệt chốt a/b/c)
```
Cách này khác guard B2 mà Decision 2 đã bác (B2 = Forbidden cứng lên self-approve); guard hẹp này chỉ chạm hành vi self-finalize.

---

### 3. Ghi chú triển khai A1 (liên quan governance)

- **Phải sửa ĐỒNG THỜI cả logic advance (SVC:645-680) lẫn `bypassedOrders` (SVC:595) + `k` (SVC:592).** Nếu chỉ đổi `bypassedOrders` mà advance vẫn nhảy vượt `k`, ô của creator sẽ bị bỏ qua mà chưa ai ký → phiếu kẹt hoặc mất chữ ký cấp creator. Yêu cầu: chỉ auto-sign `order < drafterSlots.Min`, set pointer = `drafterSlots.Min` (không advance qua).
- Điểm tích cực governance: chuyển từ auto-bypass ký-hộ (comment đóng hộp) sang creator ký tay comment thật (`SignedByUserId=creator`) là **nâng cấp** minh bạch audit, không phải nới lỏng.

---

### 4. Mục phụ (anh Kiệt nắm — không chặn deploy)

- **Admin ghi trên phiếu terminal:** `AdjustPurchaseEvaluationBudgetCommandHandler` bỏ TOÀN BỘ kiểm phase cho Admin (FEAT:367 `if (!isAdmin)`) ⇒ Admin sửa ngân sách được trên phiếu `DaDuyet`. Các endpoint suggested-price / ccm-period cũng theo pattern role-gate tương tự. Nếu "không thao tác gì được" (spec:8) hiểu chặt thì đây là đường ghi-terminal còn lại của Admin. Giảm nhẹ: `ApprovedPriceAmount` là snapshot (SVC:978) nên sửa giá sau finalize không đổi số đã chốt. Xem openQuestion 2.
- **Pre-existing minor (không thuộc thay đổi này):** reject từ `DangSoanThao`/`TraLai` — `EnsureCanRejectV2Async` :321 bỏ actor-scope cho phase ≠ ChoDuyet ⇒ user bất kỳ có thể đẩy phiếu Nháp của người khác sang TraLai (griefing). Guard đỉnh-method ở mục 1 KHÔNG che ca này (chỉ chặn DaDuyet/TuChoi). Ghi nhận, không đề xuất sửa trong scope này.

---

### 5. Test cần thêm/cập nhật (rule §7 — bug-fix + security = test-before)

- **MỚI (bắt buộc, EDGE-5):** `Transition_FromDaDuyet_RejectToTraLai_Throws`, **`Transition_FromDaDuyet_AdminApprove_Throws`** (đúng đường SVC:290 mà spec bỏ sót), `Transition_FromTuChoi_AnyDecision_Throws`. Grep xác nhận HIỆN CHƯA có test terminal-lock reject nào.
- **CẬP NHẬT (spec-change A1):** `PeSubmitGuardAndBypassTests.cs` — `Submit_OneStepWorkflow_DrafterIsLastLevel_TerminalDaDuyet` (:580, nay phải giữ `ChoDuyet` chờ creator ký), assertion `HaveCount(2)`/`HaveCount(1)` bypass-count (:505/:540), test resubmit-terminal (:645-675).
- Nếu chọn guard finalize (2c): thêm test creator-finalize-slot bị chặn/cảnh báo.

---

### 6. Rủi ro DEPLOY (prod ERP live)

- **A1 + EDGE-5 = code-only, không migration** ⇒ rủi ro thấp. Decision 6 (không migrate phiếu cũ) an toàn: phiếu cũ giữ comment auto-bypass, không đụng schema.
- **Decision 5 (opinion history) MỚI cần migration** — đây là mảnh rủi ro cao hơn, thuộc lăng-kính schema/history (đã có lane riêng: xem Area-memory `project_pe_signoff_history_schema_review.md`). Không thuộc phán quyết của tôi.
- **Cảnh báo:** nếu deploy mà đặt guard EDGE-5 theo spec (chỉ reject-branch), Decision 3 KHÔNG thực sự đạt (còn hở Admin) → sẽ là một "done" giả. Phải dùng guard đỉnh-method mục 1.

**Kết luận:** thiết kế đúng hướng và các adjustment đều nhỏ, khả thi ⇒ **GO-WITH-ADJUSTMENTS** (không NO-GO): (a) đổi vị trí guard EDGE-5 sang đỉnh-method SVC:53, (b) anh Kiệt chốt openQuestion 1 (CEO-skip) trước khi fan-out.

## Open Questions
Các quyết định còn lại thuộc owner (anh Kiệt) — cần chốt TRƯỚC khi fan-out implement + deploy:

1. **[PRIMARY — chặn implement câu 2] CEO-skip qua ô finalize của creator:** Decision 2 "tạo và duyệt luôn được" có bao gồm việc creator TỰ KẾT THÚC phiếu (bỏ CEO) khi ô Cấp của họ bật `AllowApproverFinalize` không, hay chỉ áp cho cấp KHÔNG-terminal? A1 mở đường này MỚI (trước không có với workflow đa-bước-có-CEO). Chọn: (a) chấp nhận như hiện tại; (b) cảnh báo mềm trên UI khi creator sắp tự-finalize; (c) guard hẹp chặn self-finalize (actor==Drafter) tại SVC:867 + SVC:805 — khác guard B2 mà anh đã bác vì chỉ chạm self-FINALIZE, không chạm self-approve.

2. **[Phạm vi Decision 3]** "Đã duyệt rồi là cứng không thao tác gì được" có mở rộng tới việc khóa Admin sửa ngân sách / giá trên phiếu DaDuyet (FEAT:367 admin bỏ kiểm phase) không, hay Decision 3 chỉ giới hạn ở transition/reject/trả-lại? (mục phụ 4)

3. **[Xác nhận hệ quả]** Guard hard-lock đỉnh-method gỡ luôn khả năng Admin-override lật phiếu terminal (SVC:290) — phiếu finalize nhầm sẽ chỉ sửa được bằng DB. Anh Kiệt xác nhận chấp nhận mất escape-hatch này? (đúng ý "cứng" nhưng cần biết rõ).

Ghi chú: Decision 5 (cơ chế lưu history opinion + migration) đã có lane schema/history phụ trách — không nằm trong phán quyết security/governance của tôi.
