# Spec — PE lũy kế "không bắt sau khi duyệt" (root cause = display-lie, KHÔNG phải lũy kế bug)

> `/fable-real investigator-codebase` engine-đắt deep-pass — run 2026-07-14 (S117).
> Model lane: `fable` requested (lead=Opus 4.8). ⚠️ #53 return-garble lần 1 → SendMessage-resume recover (verdict đầy đủ lần 2). Cross-verified bằng em-main độc-lập đọc code + investigator query prod DB (sqlcmd).

## ① Tính-năng / đề-bài
Anh Kiệt/Kenny + Bích Phượng báo (Zalo): phiếu PE/2026/A/018 "8 Mat Gạch xây…" (work-item MAT-8) **anh Chương duyệt rồi** nhưng phiếu MỚI cùng hạng mục (A/028, Nháp) hiển thị Row **"2. Ký trước đã chọn thầu" = "Chưa chọn"** → "duyệt rồi mà không bắt lũy kế", xóa+tạo lại vẫn không được. Nguyên tắc Kenny: **"duyệt rồi mới bắc lũy kế"**.

## ② Cách implement (verified root cause + fix)

**ROOT CAUSE (CONFIRMED, đảo chiều giả thuyết ban đầu):**
- Prod DB (sqlcmd): A/018 `Phase=10 (ChoDuyet)`, `EndedByLevelFinalize=0`, pointer `StepIdx=2/LvlOrd=1` = **đang chờ CEO (Bước 3)**; `IsWinner=1`, `SelSum=749.564.000đ`.
- Anh Chương (Bước 2 Cấp 2, `AllowApproverFinalize=1`) duyệt với `applyLevelFinalize=false` (S97 đảo default ô "Cấp này KẾT THÚC" → bỏ-tick/opt-in) → rơi xuống **advance thường** (`PurchaseEvaluationWorkflowService.cs:912-943`) → lên CEO, KHÔNG finalize.
- Changelog A/018 mới nhất: "Hoàn tất Bước 2/3, sang Bước 3 (Cấp 1)" — KHÔNG có marker "[Duyệt KẾT THÚC tại…]" (`:869`).
- `CeoApprovalThreshold=NULL` cho QT-DN-V2-001 v04 → nhánh `finalizeByCcmDelegation` (`:874-909`) không dùng được.
- Filter lũy kế Row 2 (`PurchaseEvaluationFeatures.cs:906-907`) `Phase==DaDuyet && Any(IsWinner)` **loại A/018 ĐÚNG** (chưa duyệt xong). Tính LIVE (`:886-918`), không store → xóa/tạo lại A/028 vô ích.
- ⇒ **Lũy kế KHÔNG có bug.** Bug thật = **display-lie**: `endsBeforeCeo` cho phiếu non-DaDuyet trả config `true` mù (`PurchaseEvaluationFeatures.cs:1152-1154` literal `: true`; list `:647-651` subquery `.Any()`) → sơ đồ làm mờ Bước 3 "Kết thúc tại Cấp 2" + chip "Kết thúc·CCM" NGAY KHI phiếu đang nằm inbox CEO → user tưởng đã xong (gotcha #78 nửa-còn-lại).

**FIX (2 tầng, chờ owner chốt direction):**
- **C1 — vận hành (giải quyết A/018 NGAY, 0 code):** để A/018 đạt DaDuyet → CEO Nguyễn Văn Trường duyệt Bước 3, HOẶC trả-lại → Chương duyệt lại CÓ tick "Cấp này KẾT THÚC". Xong là lũy kế bắt tự động (live) — KHÔNG cần xóa/tạo lại A/028.
- **C2 — code (chống tái diễn, display-lie):** `endsBeforeCeo` pointer-aware cho phiếu non-DaDuyet: nếu `CurrentWorkflowStepIndex` đã QUA cấp finalize (không dừng ở đó) → `endsBeforeCeo=false` → Bước 3 hiện "đang chờ CEO" thay vì mờ "Kết thúc". Detail projection (in-memory, trivial) + list projection (EF-subquery, kềnh hơn) + FE gate.

**BÁC:** Option A (ép finalize→DaDuyet — phá opt-in S97) · Option B (nới filter lũy kế nhận phiếu chưa-DaDuyet — vi phạm nguyên tắc Kenny).

## ③ Checklist (đo được — chờ owner chốt C2 hay chỉ C1)
- [ ] (C1) UAT: CEO duyệt A/018 (hoặc Chương re-approve tick-finalize) → verify `Phase=DaDuyet` → mở A/028 kiểm Row 2 = 749.564.000đ.
- [ ] (C2 nếu làm) Detail `PurchaseEvaluationFeatures.cs:1149-1154` endsBeforeCeo pointer-aware (in-memory).
- [ ] (C2) List `:645-651` endsBeforeCeo pointer-aware (EF-subquery — cân so IsWinner-subquery hiện có).
- [ ] (C2) FE gate PeFinalizeChip + PeWorkflowPanel mờ-bước theo cờ mới.
- [ ] (C2) Test `PeFinalizeProjectionTests`: ChoDuyet + config-finalize + pointer QUA cấp finalize → `endsBeforeCeo=false` (test-before, bug-fix).
- [ ] Edge multi-winner S114: Row 2 = SUM Quote.IsSelected per Detail×Supplier — độc lập số winner (OK).
- [ ] Edge negative-quote S116: Row 2 SUM có thể âm — không clamp, xác nhận đúng ý.
- [ ] Edge phiếu cũ WorkItemId=null: khối lũy kế skip — không hồi quy.

## ✅ EXECUTED (S117 — owner chọn "Sửa hiển thị + giữ opt-in", BE-only)
- Fix `PurchaseEvaluationFeatures.cs` 3 site `endsBeforeCeo` non-DaDuyet → **pointer-aware** (chỉ true khi CÒN cấp finalize ở-hoặc-SAU con-trỏ): detail `:1152` (in-memory, robust index) + list `:647` + inbox `:767` (EF subquery, rank=Step.Order-1). DaDuyet giữ `EndedByLevelFinalize` (S97).
- **0 FE** (FE = pure consumer of flag: `PeFinalizeChip`/`PeWorkflowPanel:124` gate trên `endsBeforeCeo`) · **0 migration**.
- Test-before: `PeFinalizeProjectionTests.ListPe_EndsBeforeCeo_ChoDuyet_PointerAware_PastFinalizeIsFalse` (repro A/018 con-trỏ QUA finalize → false; +2 case before/at → true) → RED trước fix (found True) → GREEN sau. EF subquery translate SQLite OK.
- Full suite **496 PASS** (Domain 45 + Infra 451, +1). `Sum()`/negative-quote/multi-winner không đụng (chỉ đổi cờ hiển thị).
- A/018 (C1 operational) = chờ anh: CEO duyệt Bước 3 hoặc trả-lại→Chương tick-finalize.

## Cross-impact + confidence
- C1 = zero code-risk. C2 = chạm `endsBeforeCeo` (dùng ở list-chip + workflow-panel + banner); HĐ-gen (`CreateContractFromEvaluation`) KHÔNG đọc endsBeforeCeo → an toàn; KHÔNG đụng filter lũy kế → nguyên tắc Kenny giữ; chỉ mở rộng runtime-read sang nhánh ChoDuyet (KHÔNG phá S97).
- Confidence: **CAO** (prod DB + changelog + code-path gate rõ; điểm chưa 100% = không thấy UI-state lúc Chương bấm, suy từ default-unchecked + kết quả advance).
