**HEADLINE:** Luồng hiện tại: ApproveV2Async làm UPSERT-opinion + advance-pointer trong CÙNG 1 call ("save = đẩy thẳng"). Điểm CHẶN "creator comment ô của mình rồi save" = `ApplyDrafterBypassOnSubmitAsync` — lúc SUBMIT nó TỰ ký Cấp-1 của creator bằng comment canned + nhảy pointer qua luôn.

## LANE A — Luồng hiện tại (create → sign-off → advance)

Tất cả file:line từ `PurchaseEvaluationWorkflowService.cs` (viết tắt `SVC`), `PurchaseEvaluationFeatures.cs` (`FEAT`), `PurchaseEvaluationsController.cs` (`CTRL`), `ApprovalWorkflow.cs` (`AW`).

### Sơ đồ luồng (file:line từng bước)

```
[CREATE] CreatePurchaseEvaluationCommandHandler (FEAT:61-160)
  Phase = DangSoanThao                         FEAT:139
  DrafterUserId = currentUser.UserId           FEAT:147   ← creator == DrafterUserId (dùng làm ID logic)
  ApprovalWorkflowId = request.ApprovalWorkflowId (pin V2)  FEAT:149  (user chọn ở PeWorkspaceCreateView:176-177)
  CurrentWorkflowStepIndex = null, CurrentApprovalLevelOrder = null  (không set)
  → Phiếu Nháp, CHƯA có pointer.

[SUBMIT] POST /transitions (CTRL:130-143) → TransitionAsync, nhánh Drafter trình (SVC:150-265)
  Guard: reject/tuchoi (SVC:67-89) · Section-3 đủ data (SVC:175-229) · require-workflow (SVC:238-241)
  Phase = ChoDuyet                             SVC:243
  CurrentWorkflowStepIndex = 0                  SVC:249
  CurrentApprovalLevelOrder = 1 (vì V2 pin)     SVC:251
  ►► ApplyDrafterBypassOnSubmitAsync(...)  ◄◄   SVC:260-261   ***ĐIỂM CHẶN***

[BYPASS] ApplyDrafterBypassOnSubmitAsync (SVC:571-681) — V2-only, chạy MỌI lần submit
  drafterSlots = firstStep.Levels WHERE ApproverUserId == DrafterUserId   SVC:589
  if drafterSlots rỗng → return (creator NGOÀI bước đầu → flow thường)     SVC:590
  k = max Order của drafter slots                                          SVC:592
  ∀ order 1..k:  + Approval row Decision=AutoApprove                       SVC:603-614
                 + UPSERT LevelOpinion comment CANNED "(duyệt tự động —     SVC:619, 629-641
                   người soạn phiếu là người duyệt cấp này)"
  Advance pointer:  k<max → CurrentApprovalLevelOrder = order kế >k        SVC:647-656
                    còn Bước → (Step 1, Cấp 1)                             SVC:657-666
                    hết → Phase=DaDuyet terminal                          SVC:667-680

[SIGN-OFF] POST /transitions decision=Approve → ApproveV2Async (SVC:686-958)
  currentIdx = CurrentWorkflowStepIndex ?? 0    SVC:711
  currentLevelOrder = CurrentApprovalLevelOrder ?? 1   SVC:715
  pendingLevelGroup = Levels Order==currentLevelOrder (OR-of-N)   SVC:724
  GATE (SVC:728-739): !admin → actor.Id ∈ pendingLevelGroup.ApproverUserId else 403 Forbidden
  + Approval row                                SVC:745-754
  ►► UPSERT LevelOpinion (matchingLevel theo ApproverUserId, comment sync)  SVC:763-790
  (skipToFinal SVC:805-846 · AllowApproverFinalize SVC:867-886 · CCM-deleg SVC:888-924)
  ►► ADVANCE pointer  SVC:927-957: currentLevelOrder<max → ++Cấp; else Bước kế Cấp1 / DaDuyet
  → UPSERT opinion + advance = CÙNG 1 call ApproveV2Async ("save = đẩy thẳng").
```

### 1. Comment UPSERT + advance xảy ra thế nào? Cùng call? Gate gì?
CÙNG 1 call `ApproveV2Async`: UPSERT opinion (SVC:763-790) rồi advance pointer (SVC:927-957) tuần tự trong cùng method, cùng `SaveChangesAsync` ở caller (SVC:285). Gate duy nhất trước khi ghi = actor phải thuộc pendingLevelGroup (SVC:728-739). Đúng như spec mô tả "save thao-tác duyệt = comment + advance cùng lúc".

### 2. Ai là approver Cấp-1?
`ApprovalWorkflow` (AW:21) > `Steps` OrderBy Order (AW:70-78, Bước=Phòng) > `Levels` group by Order (AW:83-88, Cấp) > `ApproverUserId` (AW:88, 1 NV/row, N rows cùng Order = OR-of-N). Cấp-1 Bước-1 = `steps[0].Levels WHERE Order==1`; approver = tập ApproverUserId đó.

### 3. Creator (PE.CreatedBy) = approver Cấp-1 được không? Có gate actor.Id==ApproverUserId chặn không?
- `DrafterUserId == creator == CreatedBy` (set chung lúc create FEAT:147). Service dùng **DrafterUserId** (KHÔNG dùng CreatedBy) cho mọi logic identity.
- **ĐƯỢC** — không có luật nào cấm creator làm approver. Ngược lại hệ thống có nhánh RIÊNG cho đúng case này: `ApplyDrafterBypassOnSubmitAsync`.
- Gate `actor.Id==ApproverUserId` tại SVC:728-739 là gate **THUẬN** (chỉ approver của Cấp mới được duyệt) — nó KHÔNG chặn creator; nếu pointer đang ở Cấp của creator thì gate này CHO PHÉP. **Đây KHÔNG phải điểm chặn.**

### 4. Sau create/submit phiếu ở Phase nào + CurrentApprovalLevelOrder nào (đã nhảy qua Cấp-1 chưa?)
| Mốc | Phase | Pointer (Step, Cấp) |
|---|---|---|
| Sau CREATE | DangSoanThao | (null, null) |
| Sau SUBMIT, creator KHÔNG phải Cấp-1 approver | ChoDuyet | (0, 1) — chờ Cấp-1 |
| **Sau SUBMIT, creator LÀ Cấp-1 approver (case spec)** | ChoDuyet (hoặc DaDuyet nếu 1-bước) | **(0, k+1) hoặc (1,1)** — **ĐÃ NHẢY QUA Cấp-1** |

Trong case mục tiêu, pointer **đã vượt Cấp-1** ngay tại submit, và ô C;ấp-1 của creator đã bị auto-fill comment canned (SVC:619).

### 5. ĐIỂM CHẶN (chốt)
**`ApplyDrafterBypassOnSubmitAsync` (SVC:571-681, gọi tại SVC:260-261).** Vì lúc SUBMIT nó (a) auto-ký Cấp-1 của creator bằng comment CANNED thay vì comment thật (SVC:619), và (b) advance pointer qua Cấp-1 (SVC:647-680), nên creator:
- KHÔNG bao giờ gõ được ý kiến của mình vào ô của mình (ô đã auto-fill);
- KHÔNG còn thao tác được ở Cấp-1 — pointer đã dời. FE `actorInV2Level` = false → `blockedByV2Level` (PeWorkflowPanel.tsx:98-103); BE gate SVC:728-739 cũng 403 vì pointer không ở Cấp của họ.

Nguồn gốc: đây là feature UAT S60 (anh Kiệt trước đó: "Trưởng phòng tạo thì bypass — không cần NV duyệt lại", comment SVC:550-570). Nay yêu cầu mới NGƯỢC lại.

**Quan trọng — sửa 1 chỗ là đủ:** nếu vô hiệu/không chạy bypass (để pointer giữ (0,1) sau submit cho creator-approver), luồng MONG MUỐN chạy được NGAY với **ZERO thay đổi trong ApproveV2Async** — logic "comment-in-own-box → save → advance" ĐÃ có sẵn (UPSERT SVC:763-790 + advance SVC:927). ApproveV2Async vốn hỗ trợ đúng hành vi spec cần.

FE xác nhận: `currentApproval.approvers` = approver tại pointer HIỆN TẠI (resolve giống `ResolveV2InboxIdsAsync` FEAT:786-821, match `l.Order==CurrentApprovalLevelOrder && ApproverUserId==userId`). Sau bypass đó là Cấp-2 → FE ẩn nút Duyệt của creator.

### 6. Reject/Trả lại → trả về đúng người đó comment lại rồi forward (spec item 3)
Primitive ĐÃ tồn tại qua 4 mode `ApplyReturnModeAsync` (SVC:348-548). 3 mode OneLevel/OneStep/Assignee giữ Phase=ChoDuyet + lùi pointer về Cấp trước → người đó duyệt lại (UPSERT ghi đè opinion cũ SVC:784-790) rồi forward tiếp. Mode Assignee (SVC:485-510) target bất kỳ NV đã ký. Nhưng lưu ý: Cấp-1 của creator một khi bị bypass mang comment auto, không phải comment thật.

## Open Questions
1. **Xung đột trực tiếp với UAT S60:** bypass drafter (anh Kiệt cũ: "Trưởng phòng tạo thì bypass") NGƯỢC với yêu cầu mới (creator/PRO comment ô của mình rồi save). Owner chốt: (a) bỏ hẳn bypass, (b) chuyển bypass thành opt-in/opt-out per-workflow hoặc per-level, hay (c) giữ bypass cho drafter-không-phải-Cấp-1 nhưng để creator-Cấp-1 comment tay. Đây là đảo ngược hành vi S60 — cần sign-off rõ.
2. Khi creator LÀ Cấp-1: submit nên để pointer ở (0,1) rồi creator bấm Duyệt riêng (2 click), hay gộp 1 action "submit + duyệt-Cấp-mình"? (1 click).
3. Data cũ: các phiếu đã submit trước đây có Approval row Decision=AutoApprove + LevelOpinion comment canned do bypass ghi — cần migrate/cleanup hay chỉ áp cho phiếu mới?
4. Nếu bỏ bypass, các quy trình mà TP/Trưởng phòng tự tạo sẽ phải tự bấm duyệt Cấp mình — xác nhận đây là UX chấp nhận được (đúng ý spec "comment vào ô duyệt CỦA CHÍNH HỌ").
