---
name: pe-multiwinner-perhangmuc-review
description: S(ensemble) review PE cardinality-change #3 single→multi (Supplier.IsWinner whole-pkg → Quote.IsSelected per-hạng-mục); consumer-completeness lens
metadata:
  type: project
---

# PE multi-NCC per hạng mục (cardinality single→multi, trục C Quote.IsSelected) — consumer-completeness review

**Verdict: PASS_WITH_ADJ.** 6 BE read-site inventory COMPLETE — independent grep của `IsWinner`/`winnerQuoteTotal`/`SelectedSupplierId` khắp `src/Backend` map 1:1, KHÔNG sót financial read-site (khác S87 sót 2). Nhưng 2 MUST-precision.

**MUST caught:**
1. **giaTri silent-financial** — CreateContractFromEvaluationFeatures.cs:89-93 `isSingle ? detailsSum : SUM(all w quotes)`. Dưới per-hạng-mục, single derived-winner ≠ whole-package → `detailsSum` over-count hạng-mục-không-thắng → contract GiaTri SAI ÂM THẦM. CẢ 2 nhánh phải = `SUM(w quotes WHERE IsSelected)`; drop isSingle special-case. Spec "SUM IsSelected của NCC" đúng nhưng implementer dễ chỉ sửa nhánh else.
2. **Derive-completeness** — D2 giữ Supplier.IsWinner DERIVED cho display+tạo-HĐ, nhưng derive chỉ wired trong SelectWinner. DbInitializer seed:1310-1316 set SelectedSupplierId + Quote.IsSelected per-cell nhưng KHÔNG BAO GIỜ set IsWinner (grep IsWinner = 0 hit DbInitializer). Seeded DaDuyet phiếu → total đúng (SUM IsSelected) NHƯNG winner-names rỗng + CreateContract:56-58 throw "chưa chọn NCC". Backfill 1-chiều (IsWinner→IsSelected) thiếu chiều ngược (IsSelected→IsWinner derive). Prod protected Mig 58; Dev/test/demo vỡ.

**Lesson: derived-field (IsWinner) làm source-of-truth phái sinh = mọi write-path của field-gốc (IsSelected) PHẢI re-derive.** Spec chỉ derive 1/3 path: SelectWinner ✓, UpsertQuote(:308/332) ✗, seed ✗. Failure mode = VISIBLE (empty names / block create), KHÔNG silent-money → SHOULD/MUST tùy path; nhưng giaTri branch mới là silent-financial thật.

**Confirmed clean (anti-rubber-stamp):** SlaExpiryJob Contract-only (db.Contracts :76-79, không PE); no AutoMapper/notification/export/validator đọc winner; FE 0 client-side winner-sum recompute (chỉ consume BE `ev.winnerQuoteTotal` + boolean `.some(isWinner)`) → BE re-key + derive là đủ; FE type files admin/user offset ~4 dòng (admin :120/144/167/177 vs user :124/148/171/181) xác nhận spec cảnh báo "types drift patch-delta KHÔNG blind-copy" ĐÚNG; 7/7 named test file tồn tại.
