# 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.
