# S117 — PE lũy-kế display-fix + duyệt theo khoảng Min–Max (2026-07-14)

**Commit:** `316a82f` · **CI Run #387** PASS 5m36s · **State:** Mig 66 · 89 bảng · 501 test · gotcha 81 · menu 54 · bundle admin `cFI7ih4a`/user `MxgooVZw` · prod 37 NCC/17 user.
**Mode:** HMW-ON · lead Opus 4.8 · em-main-led · 2× `/fable-real investigator-codebase` engine-đắt (Fable) + 4 spawn (test-specialist · implementer-frontend · cicd-monitor) · 1 commit.

Buổi UAT anh Kiệt FDC (Zalo, ảnh) đưa 2 việc PE — anh chỉ-định `/fable-real` điều-tra rồi fix.

## (1) Fix hiển thị lũy-kế "duyệt rồi không bắt" — root cause = DISPLAY-LIE, KHÔNG phải lũy-kế bug

**Triệu chứng (anh Kiệt/Kenny/Bích Phượng):** phiếu A/018 "8 Mat Gạch xây…" (MAT-8) anh Chương duyệt rồi nhưng phiếu mới A/028 cùng hạng mục hiện 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ô ích. Nguyên tắc Kenny: "duyệt rồi mới bắc lũy kế".

**Điều tra (`/fable-real invest`, wf `ab2657ee`):** ⚠️ #53-garble lần 1 → **SendMessage-resume** recover (verdict đầy đủ lần 2) + em-main verify prod-DB độc-lập. Kết quả **lật tiền-đề**: A/018 **CHƯA DaDuyet** — prod `Phase=ChoDuyet, EndedByLevelFinalize=0, pointer Step2/Lvl1 = đang chờ CEO (Bước 3)`. Anh Chương duyệt cấp finalize (AllowApproverFinalize=1) NHƯNG **không tick "Cấp này KẾT THÚC quy trình"** (S97 đảo default = bỏ-tick/opt-in) → phiếu **advance lên CEO** thay vì finalize. `CeoApprovalThreshold=NULL` → nhánh ccm-delegation không dùng được. Lũy-kế filter Row 2 (`PurchaseEvaluationFeatures.cs:906` `Phase==DaDuyet && Any(IsWinner)`) loại A/018 **ĐÚNG** (chưa duyệt xong). IsWinner A/018 = có (winner Thảo Anh). Tính LIVE → xóa/tạo lại A/028 vô ích (lỗi ở phiếu-nguồn).

**Bug thật = display-lie (gotcha #78 nửa-còn-lại):** `endsBeforeCeo` cho phiếu non-DaDuyet trả config `true` mù (`PurchaseEvaluationFeatures.cs:1152` literal `: true`; list/inbox `.Any()`) → sơ đồ mờ Bước 3 "Kết thúc tại Cấp 2" NGAY KHI phiếu đang nằm inbox CEO → user tưởng đã xong. S97 mới sửa cho phiếu ĐÃ DaDuyet (runtime `EndedByLevelFinalize`), bỏ sót nhánh ChoDuyet.

**Owner AskUser → "Sửa hiển thị + giữ opt-in".** Fix **POINTER-AWARE 3 site**: non-DaDuyet chỉ `endsBeforeCeo=true` khi CÒN cấp finalize ở-hoặc-SAU con-trỏ hiện tại (phiếu còn có thể finalize). Đã đi QUA → false. Detail `:1152` (in-memory, robust index `steps.IndexOf` vs currentIdx) + list `:647` + inbox `:767` (EF-subquery, rank cấp = `Step.Order-1` — Order tuần-tự s+1 seed/Designer). **BE-only** (FE là consumer thuần: `PeFinalizeChip` + `PeWorkflowPanel:124` gate cờ). +1 test repro `PeFinalizeProjectionTests.ListPe_EndsBeforeCeo_ChoDuyet_PointerAware_PastFinalizeIsFalse` (RED trước → GREEN sau). A/018 lên lũy-kế = việc vận-hành (CEO duyệt / re-approve tick).

## (2) Feature "duyệt theo khoảng Min–Max"

**Yêu cầu (anh Kiệt):** ô "CHỌN GIÁ CHỐT" khi duyệt cuối chọn 1 radio (Ncc/ProMin/ProMax/Ccm). Khi PRO có cả Min & Max → cho **"chọn cả 2 min và max"** = duyệt theo khoảng [Min,Max].

**Điều tra (`/fable-real invest`, wf `abbbb86d`, 0-garble clean verdict) → Model A:** +cột `ApprovedPriceMaxAmount` + source token `"ProMinMax"` radio thứ 5 (chỉ khi cả 2 giá PRO ≠ null). Amount=Min, MaxAmount=Max. Bất-biến `MaxAmount != null ⟺ Source=="ProMinMax"` + Min≤Max. **Linchpin verify độc-lập:** `CreateContractFromEvaluationFeatures.cs:88` GiaTri = SUM báo giá NCC được chọn, `grep ApprovedPrice` = 0-match → **HĐ KHÔNG đọc giá chốt** → range thuần record-of-decision, **0 tác động HĐ**.

**Implement (anh "làm theo hướng dẫn nó luôn"):**
- BE **em-main-solo** (financial + signature ripple): entity + EF config + Mig 66 (AddColumn nullable no-backfill, reversible) + service (`ValidApprovedPriceSources` +ProMinMax · `ApplyApprovedPriceOnFinalize` +param maxAmount + validate · 3 call-site :858/:896/:928 + 2 method sig) + command/validator/handler + controller body + DTO + projection + interface.
- FE **implementer-frontend** 2-app SHA-mirror `011ee8cf`/`617b0aa0` (Panel radio thứ 5 + submit `approvedPriceMaxAmount` chỉ khi ProMinMax → axios omit key với source đơn = khớp BE reject · DetailTabs "min – max") + types ×2 (không mirror).
- Test **test-specialist** +5 (happy/min==max-boundary/thiếu-Max-Conflict/min>max-Conflict/single+Max-wire-bug + regression). Harness fix: `InvokeApply` reflection 4→5 args (optional param không auto-bind `MethodInfo.Invoke` → tránh `TargetParameterCountException` phá 3 test cũ).
- 3 product-decision investigator flag → resolve theo khuyến nghị (HĐ giữ nguyên · range dùng chung 3 finalize-path · 1 radio khớp "chọn cả 2").

## Verify + deploy
- BE build 0-err · full suite **495→501** (Domain 45 + Infra 456) · Mig 66 applied Dev (+ caught up Mig 65 unapplied-local drift) · FE 2-app build sạch · SHA-mirror khớp · submit logic review đúng contract · diff 18 file (pre-existing "M" = CRLF, verify all-mine).
- Commit `316a82f` push main → **cicd Run #387 PASS**: test gate 501 · Mig 66 prod (`ApprovedPriceMaxAmount` decimal(18,2) + history top + 89 bảng) · bundle rotate ×2 (CSS frozen=logic-only) · byte-level `ProMinMax`/`duyệt theo khoảng`/`cả Min và Max`/`Min–Max` PRESENT · smoke 200×3 · PE 401.
- ⚠️ **#53-garble ×2** (invest-lane-1 + cicd-monitor) → SendMessage-resume + **em-main curl độc-lập cross-verify** (`cFI7ih4a`/`MxgooVZw` khớp verdict agent) = verdict tin-cậy dù agent-return hỏng.

## Lessons
- **premise-flip → surface-before-code:** báo-lỗi "lũy kế không bắt" hóa ra là ĐÚNG-behavior (phiếu chưa duyệt xong); điều-tra lật tiền-đề, hỏi owner TRƯỚC khi code (tránh sửa nhầm filter/đảo S97). Real bug = display-lie.
- **HĐ-independent-verify-before-model:** grep ApprovedPrice=0 ở HĐ-gen = bằng chứng range an-toàn → chọn Model A (chỉ record-of-decision) không lo tác động tài-chính.
- **#53-garble WRITE-lane ≠ mất-code** (code trên disk) + **cicd cross-verify em-main-curl** (2 nguồn độc-lập khớp) = pattern verify đáng-tin.
- **signature-ripple financial = em-main-solo** (3 call-site + 2 method), delegate mechanical FE-mirror + test.
- reflection-test `MethodInfo.Invoke` không auto-bind optional param → đổi arity helper phải cập-nhật (test-specialist bắt).

## NEXT
**Anh (UAT prod INCOGNITO — #77):** (1) phiếu chờ CEO → sơ đồ hết mờ "Kết thúc tại Cấp 2"; (2) đưa A/018 lên Đã duyệt → A/028 hiện lũy kế 749.564.000đ; (3) phiếu PRO Min–Max → radio "PRO — cả Min và Max" → chi tiết "min – max"; (4) carry BVAAU.
**Em:** commit closeout · watch AI_INFRA 3 email · inv-cb L1 curate · monthly re-tier 2026-08-01.
