---
name: s114-pe-multi-ncc-perhangmuc-financial-review
description: S114 PE multi-NCC per-hạng-mục (winner-truth Quote.IsSelected, IsWinner derived) — refute-BE-financial lens; NEEDS-FIX 1 MUST (delete paths strand invariant)
metadata:
  type: project
---

# S114 PE multi-NCC per-hạng-mục — refute-BE-financial (fable-clone reviewer lane)

Verdict **NEEDS-FIX** (1 MUST). Winner-truth đổi whole-supplier `Supplier.IsWinner` → per-hạng-mục `Quote.IsSelected`; IsWinner nay DERIVED == Any(quote.IsSelected).

## Financial SUM axis = CORRECT (đã verify grep độc lập)
6 SUM site đều key `q.IsSelected` (không sót IsWinner-keyed sum nào toàn `src/Backend`): winnerQuoteTotal `PeFeatures.cs:1188` · currentProposalTotal:920 · prevSelectedTotal:909 · submit-guard `WorkflowService.cs:196` · CCM-threshold:882 (`>= ceoThreshold` boundary GIỮ, đúng) · giaTri per-winner `CreateContract:88`. Grep `IsWinner` toàn BE = CHỈ existence/name-projection (List:186, PeFeatures:637/758, "đã chọn thầu":907) + entity/config/migration — KHÔNG financial-sum. Backfill 2-chiều idempotent + total-preserving. Seed derive + SelectWinner derive + SelectedSupplierId sync đúng.

## MUST — A2 invariant KHÔNG maintain trên DELETE paths
`supplier.IsWinner == supplier.Quotes.Any(IsSelected)` chỉ re-derive ở SelectWinner/UpsertQuote(ins+upd)/seed/backfill. **DeleteQuote `PurchaseEvaluationDetailFeatures.cs:396` + DeleteDetail:262 (cascade Detail→Quotes, config line118) xóa selected-quote KHÔNG re-derive IsWinner** (cũng không re-sync SelectedSupplierId). Xóa quote-selected cuối của 1 NCC → IsWinner stranded=true, 0 selected. Reachable FE: quote-dialog "Xóa" btn `PeDetailTabs.tsx:2897` (unconditional, mọi existing quote) + xóa-hạng-mục. Hậu quả: CreateContract `:56` winners chứa phantom → HĐ GiaTri=0 cho NCC không-winner + winner-name/existence sai + FE row kẹt (canDelete=`!isWinner && !hasQuotes`:2640 → không xóa được). Self-heal 1-phần: SelectWinner/UpsertQuote kế tiếp re-derive, NHƯNG không đảm bảo trước CreateContract. **0 test** (không test-source nào gọi DeletePurchaseEvaluationQuote/DetailCommand).

## Lesson
Derived-invariant (IsWinner từ IsSelected) phải re-establish ở **MỌI mutation đổi tập nguồn — kể cả DELETE + cascade-delete**, không chỉ write/select. Acceptance liệt kê maintenance-point (SelectWinner+Upsert+seed+backfill) BỎ SÓT delete = đúng chỗ reviewer phải bắt (S87/S88 class mở rộng: mutation-path miss ≡ read-site miss). Grep write-path field-NGUỒN + đếm MỌI `.Remove(` chạm entity chứa nguồn.
