diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index 6ec9845..beb142d 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -101,6 +101,9 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte --- +## S118 — PE giá-chốt RANGE Min+Max invest (fable-real single, 07-14) +- ApprovedPrice WRITE = 1 site duy nhất `ApplyApprovedPriceOnFinalize` (`PurchaseEvaluationWorkflowService.cs:951-966`, allow-list :950) gọi tại 3 nhánh DaDuyet :858 level-finalize / :896 ccm-delegation / :928 all-steps-done (skipToFinal :820 chỉ dời pointer, KHÔNG finalize-site thứ 4). READ prod = detail-DTO ONLY (`PurchaseEvaluationFeatures.cs:1230` → `PurchaseEvaluationDtos.cs:285`) + FE display `PeDetailTabs.tsx:1712/:1850-1863`. ⭐ **HĐ-gen KHÔNG đọc ApprovedPrice** — GiaTri=SUM Quote.IsSelected per-winner (`CreateContractFromEvaluationFeatures.cs:88-100` LOCKED A1/D4) → range 0-impact contract/lũy-kế/list. Rec model A: +1 col nullable `ApprovedPriceMaxAmount` + source token `ProMinMax` = radio thứ-5 (chỉ khi cả Min+Max present), no backfill, phiếu cũ nguyên. PRO-entry đã enforce Min≤Max (`PeSuggestedPriceFeatures.cs:38-40`). Panel+DetailTabs SHA-mirror 2-app NHƯNG `types/purchaseEvaluation.ts` KHÔNG mirror (SHA differ) — edit riêng từng app. Latent edge: winnerQuoteTotal subset-selected có thể ÂM (S116) vs validator `ApprovedPriceAmount>=0` `:501` → transition reject khó hiểu. WIP S117 uncommitted CÙNG file PurchaseEvaluationFeatures.cs (endsBeforeCeo pointer-aware :642/:768/:1157) — vùng khác, merge-safe. `[s118, pe-approved-price-range, prominmax]` + ## S117 — PE lũy-kế "không bắt sau duyệt" RCA (fable-real single deep-pass) - **2026-07-14** VERDICT: phiếu nguồn A/018 KHÔNG hề DaDuyet — Chương (Bước2 Cấp2 `AllowApproverFinalize`, badge "Duyệt thay CEO" = CONFIG-display `PeWorkflowPanel.tsx:364`) duyệt KHÔNG tick ô OPT-IN "Cấp này KẾT THÚC" (S97 Part1 đảo default UNCHECKED `PeWorkflowPanel.tsx:57` useState(false); FE gửi tường minh :203) → advance thường lên Bước3 CEO, Phase=10 ChoDuyet. Prod ground-truth: StepIdx=2 LvlOrd=1 EndedFin=0; changelog 13/07 12:23Z "Hoàn tất Bước 2/3, sang Bước 3 (Cấp 1)" no-finalize-marker; IsWinner=1 SelQ=1 (749.564M) → RC-2 refuted. Lũy-kế Row2 filter `Phase==DaDuyet` (`PeFeatures.cs:906`) ĐÚNG nguyên tắc Kenny "duyệt rồi mới bắc"; tính LIVE :886-918 no-store → xóa+tạo-lại phiếu-XEM vô ích. ⭐ Residual = DISPLAY-LIE nửa-còn-lại gotcha #78: non-DaDuyet `endsBeforeCeo`=config TRUE bất chấp pointer (detail `:1152-1154` literal `: true`; list `:647-651` subquery `.Any()`) → Bước3 mờ "Kết thúc tại Cấp 2" + chip list "⚑ Kết thúc·CCM" NGAY KHI phiếu nằm inbox CEO → user tưởng xong. 3 cơ chế chồng phân biệt: AllowApproverFinalize (Mig58 + opt-in S97) ≠ finalizeByCcmDelegation (Mig53/54 threshold — v04 CeoThresh=NULL không dùng) ≠ skipToFinal (Mig31 pointer-jump giữ ChoDuyet). Fix rec: vận-hành CEO-duyệt hoặc trả-lại→Chương tick; code = pointer-aware endsBeforeCeo (detail in-memory trivial, list EF-subquery kềnh), KHÔNG nới filter lũy kế. `[engine: fable-real-single · Fable-S117]` diff --git a/.claude/workflows/runs/2026-07-14-S117-pe-approve-minmax-range/spec-pe-approve-minmax-range-14-07-2026.md b/.claude/workflows/runs/2026-07-14-S117-pe-approve-minmax-range/spec-pe-approve-minmax-range-14-07-2026.md new file mode 100644 index 0000000..d765ebd --- /dev/null +++ b/.claude/workflows/runs/2026-07-14-S117-pe-approve-minmax-range/spec-pe-approve-minmax-range-14-07-2026.md @@ -0,0 +1,38 @@ +# Spec — PE duyệt cấp cuối: chọn CẢ Min và Max (duyệt theo khoảng) + +> `/fable-real investigator-codebase` engine-đắt deep-pass — run 2026-07-14 (S117 task 2). Model lane: fable (0-garble, clean verdict). Lead verify: HĐ-gen linchpin CONFIRMED độc-lập (`CreateContractFromEvaluationFeatures.cs:88-90` GiaTri=SUM selected NCC quotes, grep ApprovedPrice=0-match). Owner: "làm theo hướng dẫn của nó luôn". + +## ① Tính-năng / đề-bài +anh Kiệt FDC: ô "CHỌN GIÁ CHỐT (DUYỆT THEO GIÁ ĐỀ XUẤT)" khi duyệt cấp cuối hiện radio Ncc/ProMin/ProMax/Ccm — chọn ĐÚNG 1. Yêu cầu: **khi PRO có cả Min & Max → cho chọn CẢ HAI (duyệt theo khoảng [Min,Max])**. "Chọn cả 2 min và max." + +## ② Cách implement — MODEL A (investigator khuyến nghị, verify OK) +Thêm nullable `ApprovedPriceMaxAmount` + source token `"ProMinMax"` làm **radio thứ 5** (chỉ hiện khi PRO có CẢ Min+Max). Bất-biến: `ApprovedPriceMaxAmount != null ⟺ ApprovedPriceSource == "ProMinMax"` (Amount=Min, Max=Max). Source đơn giữ nguyên (Max=null). **1 migration AddColumn nullable, no new table, NO backfill** (phiếu cũ nguyên vẹn). **HĐ.GiaTri KHÔNG đổi** (không đọc giá chốt) — range thuần record-of-decision hiển thị. + +**BE:** +- Entity `PurchaseEvaluation.cs:97-98` +`decimal? ApprovedPriceMaxAmount`. +- Config `PurchaseEvaluationConfiguration.cs` +`HasPrecision(18,2)`. +- Migration `AddPeApprovedPriceRange` (Mig 66) 3-file. +- Service `PurchaseEvaluationWorkflowService.cs`: `ValidApprovedPriceSources:950` +`"ProMinMax"`; `ApplyApprovedPriceOnFinalize:951-966` +param `decimal? maxAmount` (ProMinMax⇒require maxAmount≠null && amount≤maxAmount; source-đơn+maxAmount≠null⇒Conflict wire-bug; isSystem+null giữ); 3 call-site :858/:896/:928 + `ApproveV2Async:650-661` sig + `TransitionAsync:47-50` sig + pass-through :291. +- Interface `IPurchaseEvaluationWorkflowService.cs:33-34` +`approvedPriceMaxAmount=null`. +- `PurchaseEvaluationFeatures.cs`: Command +field; Validator (+ProMinMax vào Must; +Max≥0; +cross-rule Max⟺ProMinMax; Min≤Max); Handler pass; projection GetDetail +`e.ApprovedPriceMaxAmount`. +- Controller `PurchaseEvaluationsController.cs`: `TransitionPeBody` +field; Send +field. +- DTO `PurchaseEvaluationDtos.cs:285` +`ApprovedPriceMaxAmount`. + +**FE (2-app):** +- `types/purchaseEvaluation.ts` (⚠️ 2 app KHÔNG identical — sửa riêng) +`approvedPriceMaxAmount`. +- `PeWorkflowPanel.tsx:134-141`: +candidate `{source:'ProMinMax', label:'PRO — cả Min và Max (duyệt theo khoảng)', amount:min, amountMax:max}` chỉ khi cả 2≠null; GIỮ radio; +`selectedPriceAmountMax`; submit +`approvedPriceMaxAmount`; render "min – max". +- `PeDetailTabs.tsx:1661-1666` +`ProMinMax:'PRO Min–Max'`; :1850-1863 khi Max≠null render `min – max`. + +## ③ Checklist (financial-critical → test-before merge, rules §7) +- [ ] Entity + config + migration 3-file → ef update Dev. +- [ ] Service allow-list + helper +param + 3 call-site + 2 sig + interface. +- [ ] Command/Validator/Handler + Controller body + DTO + projection. +- [ ] Tests (`PeApprovedPriceFinalizeTests` mirror): (a) ProMinMax happy set 2 field; (b) thiếu maxAmount→Conflict not-finalized; (c) min>max→Conflict; (d) source-đơn+maxAmount→Conflict; (e) 4 source đơn regression pass. `dotnet test`. +- [ ] FE types ×2 → Panel ×2 mirror → DetailTabs ×2 mirror → npm build ×2 + SHA-check Panel/Tabs. +- [ ] Smoke: duyệt cuối chọn range → detail "min – max"; phiếu cũ nguyên. + +## Product-notes (resolved theo owner "làm theo hướng dẫn") +- HĐ.GiaTri khi range = GIỮ NGUYÊN (SUM NCC quotes, không đọc giá chốt). Cảnh-báo-mềm GiaTri ngoài [Min,Max] = **out-of-scope** (feature riêng, flag cho anh Kiệt). +- Range dùng chung 3 finalize path (level-finalize/ccm-delegation/all-steps) — tự có vì chung helper, OK. +- UI = 1 radio "cả Min và Max" (khớp "chọn cả 2 min và max"), KHÔNG checkbox combo. +- Latent (pre-existing, ghi nhận không sửa): winnerQuoteTotal âm S116 → validator ApprovedPriceAmount≥0 reject — ngoài scope.