[CLAUDE] PurchaseEvaluation: multi-NCC per hạng mục — chọn ≥2 NCC-TP trúng thầu/1 hạng mục (mỗi nhà 1 giá riêng)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m17s

Đổi winner-truth từ whole-supplier Supplier.IsWinner (Mig 58 liên-danh) sang per-hạng-mục
Quote.IsSelected (Mig 12 dormant hook → nay nguồn-sự-thật); Supplier.IsWinner = DERIVED (== Any IsSelected).
Anh Kiệt LOCKED D1=SUM(IsSelected) · D2=THAY-THẾ · D3=giá-chào-gốc · D4=1HĐ/NCC. 0 bảng/cột mới.

BE:
- SelectWinnerCommand + POST /select-winner: body {supplierIds} → {detailId, supplierIds} (per hạng mục).
- Re-key 5 financial-SUM → WHERE IsSelected: winnerQuoteTotal, submit-guard, CCM-threshold, giaTri, budget-B.
- giaTri bỏ shortcut isSingle→detailsSum (single đổi budget→bid theo D4).
- Derive IsWinner invariant ở MỌI writer: SelectWinner + UpsertQuote + seed + DeleteQuote/DeleteDetail (helper PeWinnerInvariant, fix Bước 4).
- GIỮ IsWinner-derived cho winner-names/existence + CreateContract winners-list.
- Migration BackfillPeQuoteIsSelectedAndWinnerInvariant (data-only 2-chiều IsWinner↔IsSelected, Down no-op).

FE 2-app (PeDetailTabs SHA-identical): HangMucCard per-cell winner grid Detail×NCC + toggle per-detail; NccSelectorRow hạ-cấp read-only summary; giữ derived display.

Test: 7 file update + 8 test mới (per-hạng-mục total, giaTri single-partial, threshold-crossing, derive-invariant, delete-path). 486 PASS.

Flow: fable-clone invest ensemble → anh Kiệt 4-decision → fable-clone review ensemble (6 adjustment) → hmw implement (disk-recover #53) → fable-clone verify (bắt MUST bug delete-path → fix test-before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-12 22:22:09 +07:00
parent 8a1b0d4901
commit d436af2971
18 changed files with 7272 additions and 425 deletions

View File

@ -1260,6 +1260,21 @@ public static class DbInitializer
}
await db.SaveChangesAsync();
// [multi-NCC 2026-07-12 — A2(c)] Derive IsWinner mỗi supplier-row từ grid IsSelected
// (INVARIANT: supplier.IsWinner == đơn vị có ÍT NHẤT 1 quote IsSelected). Đặt SAU khi
// tạo quotes để seed đồng bộ nguồn-sự-thật (Quote.IsSelected) với derived flag.
for (int si = 0; si < supplierEntities.Count; si++)
{
bool anySelected = false;
for (int di = 0; di < detailEntities.Count && di < quotesGrid.Length; di++)
{
var gridRow = quotesGrid[di];
if (si < gridRow.Length && gridRow[si].IsSelected) { anySelected = true; break; }
}
supplierEntities[si].IsWinner = anySelected;
}
await db.SaveChangesAsync();
// Approval history theo flow của type
PurchaseEvaluationPhase[] flow = type == PurchaseEvaluationType.DuyetNcc
? [PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM,

View File

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class BackfillPeQuoteIsSelectedAndWinnerInvariant : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// [multi-NCC 2026-07-12 — anh Kiệt LOCKED A2/A6] Data-only backfill 2 CHIỀU thiết lập
// BẤT BIẾN: PurchaseEvaluationSupplier.IsWinner == đơn vị có ÍT NHẤT 1
// PurchaseEvaluationQuote.IsSelected. NGUỒN-SỰ-THẬT mới = Quote.IsSelected (per
// Detail×Supplier); IsWinner (whole-supplier, Mig 58) → DERIVED. Idempotent (WHERE ...=0
// → re-run no-op). SQL-Server-only (tests SQLite EnsureCreated KHÔNG replay migration).
// CHIỀU 1 (IsWinner → IsSelected): phiếu cũ mô hình whole-supplier winner (Mig 58) →
// mọi báo giá của đơn vị IsWinner=1 được đánh IsSelected=1 (đơn vị thắng cả gói → mọi
// hạng mục của họ được chọn). Preserve tổng: winnerQuoteTotal cũ (SUM quote của IsWinner)
// == winnerQuoteTotal mới (SUM quote IsSelected). Mirror Mig 58 idempotent.
migrationBuilder.Sql(@"
UPDATE q SET IsSelected = 1
FROM PurchaseEvaluationQuotes q
INNER JOIN PurchaseEvaluationSuppliers s ON s.Id = q.PurchaseEvaluationSupplierId
WHERE s.IsWinner = 1 AND q.IsSelected = 0;");
// CHIỀU 2 (IsSelected → IsWinner): dữ liệu dormant/seed có Quote.IsSelected=1 nhưng
// supplier-row IsWinner=0 (per-hạng-mục selection chưa đồng bộ flag) → set IsWinner=1
// cho đơn vị có tồn tại ≥1 quote IsSelected. Sau 2 chiều: IsWinner ⟺ Any(IsSelected).
migrationBuilder.Sql(@"
UPDATE s SET IsWinner = 1
FROM PurchaseEvaluationSuppliers s
WHERE s.IsWinner = 0
AND EXISTS (
SELECT 1 FROM PurchaseEvaluationQuotes q
WHERE q.PurchaseEvaluationSupplierId = s.Id AND q.IsSelected = 1);");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Data-only backfill KHÔNG reversible (không snapshot trạng thái IsSelected/IsWinner
// trước khi thiết lập bất biến; revert = phá bất biến / mất per-hạng-mục selection).
// No-op an toàn — mirror BackfillEndedByLevelFinalizeFromChangelog (Mig 61).
}
}
}