---
name: project-s114-multi-ncc-perhangmuc-impl-review
description: S114 PE multi-NCC per-hạng-mục IMPL review (winner-truth flip lần 3) — CAUGHT MUST DeleteQuote invariant-break missed writer
metadata:
  type: project
---

S114 (2026-07-12) close-review PE multi-NCC per hạng mục — winner-truth flip whole-supplier `Supplier.IsWinner` → per-(Detail×Supplier) `Quote.IsSelected` (IsWinner nay DERIVED). Lens refute-fe-test. Verdict NEEDS-FIX (1 MUST).

**CAUGHT MUST (missed writer — S87/S88 cardinality anti-pattern lần 3):** `DeletePurchaseEvaluationQuoteCommandHandler` (`PurchaseEvaluationDetailFeatures.cs:371-397`) KHÔNG re-derive `supplier.IsWinner` sau `Remove(quote)`. Spec A2 liệt kê writer "SelectWinner + UpsertQuote(update+insert) + seed" — **DeleteQuote vắng mặt trong enumeration** → implementer theo spec faithfully nhưng spec sót. Xóa quote IsSelected cuối của 1 NCC qua nút "Xóa" (FE wired `PeDetailTabs.tsx:2897`) → invariant `IsWinner==Any(IsSelected)` VỠ → phantom winner: winnerSupplierNames projection (`where sw.IsWinner`) hiện NCC + `CreateContractFromEvaluationFeatures.cs:56` `winners=Where(IsWinner)` tạo **HĐ giá 0** (giaTri=SUM selected=0, :88-90). Reachable Draft-phase, sống tới DaDuyet.

**Lesson:** cardinality-change grep-consumer PHẢI gồm cả writer XÓA (Delete handler) của field-nguồn, KHÔNG chỉ Select/Upsert. Field-nguồn = `Quote.IsSelected` → mọi path đổi TẬP quote (thêm/sửa/**xóa**) phải re-derive flag phái sinh. Spec-enumeration của writer KHÔNG đáng tin — grep độc lập MỌI handler chạm entity (grep `Remove(.*Quote`/`db.*Quotes.Remove`).

**SHOULD:** (1) derive-invariant test (`PeSelectWinnerClearTests.cs:566`) chỉ cover UpsertQuote INSERT + SelectWinner; KHÔNG cover UpsertQuote UPDATE-path (exclusion `q.Id != existing.Id` :313-316 — subtle stale-DB-read guard) NÊN test-before-fix cũng thiếu DeleteQuote. (2) FE winner-toggle (`:2707`) không gate trên `q` tồn tại → chọn winner NCC chưa nhập giá = silent no-op + toast success sai (BE chỉ flip IsSelected quote CÓ SẴN :433-434).

**Verified SOLID (resisted scrutiny):** FE 2-app byte-identical `ae3788e9` (hash-object khớp). 5 financial-SUM đều `WHERE IsSelected` (winnerQuoteTotal/submit-guard/CCM-threshold/giaTri/budget-B). giaTri A1 bỏ detailsSum single+multi. **Test discriminate THẬT (non-tautology):** threshold-crossing dựng whole-supplier 1.2 tỷ≥ngưỡng vs selected 500tr<ngưỡng; giaTri-subset 300 vs 800(all-quote) vs 2000(budget). SelectWinner derive IsWinner toàn phiếu (giữ winner hạng mục KHÁC). Seed set IsSelected=true (`DbInitializer.cs:1385`) → derive IsWinner đúng, KHÔNG empty-winner regression (concern plan-review cũ đã đóng).
