[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

@ -189,25 +189,23 @@ public class PurchaseEvaluationWorkflowService(
// missingForApproval (FE KHÔNG check Purpose enum → BE cũng không,
// tránh mismatch 2 tầng).
var missing = new List<string>();
// [Mig 58 CCM item 1] Multi-winner: winner = các supplier-row IsWinner (≥1). Tổng giá
// gói = SUM ThanhTien MỌI winner-row (vd 3 đơn vị 37.8+9+10=56.8tr). 0 winner →
// "chưa chọn"; có winner nhưng tổng ≤ 0 → "chưa có giá chào thầu" (item 2 chỉ nới
// NGÂN SÁCH, KHÔNG nới giá NCC — winner phải có giá).
var winnerRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.IsWinner)
.Select(s => s.Id)
.ToListAsync(ct);
if (winnerRowIds.Count == 0)
// [multi-NCC A3(i)[2] + D5] Winner = có ÍT NHẤT 1 báo giá ĐƯỢC CHỌN (Quote.IsSelected
// = nguồn-sự-thật per Detail×Supplier). Tổng giá gói = SUM ThanhTien mọi quote IsSelected
// toàn phiếu (D5 giữ >0). 0 selected → "chưa chọn"; có selected nhưng tổng ≤0 → "chưa có giá"
// (item 2 chỉ nới NGÂN SÁCH, KHÔNG nới giá NCC — winner phải có giá).
var selectedThanhTien = await (
from q in db.PurchaseEvaluationQuotes.AsNoTracking()
join d in db.PurchaseEvaluationDetails.AsNoTracking()
on q.PurchaseEvaluationDetailId equals d.Id
where d.PurchaseEvaluationId == evaluation.Id && q.IsSelected
select q.ThanhTien).ToListAsync(ct);
if (selectedThanhTien.Count == 0)
{
missing.Add("chưa chọn Đơn vị NCC/TP");
}
else
else if (selectedThanhTien.Sum() <= 0)
{
var winnerQuoteTotal = await db.PurchaseEvaluationQuotes.AsNoTracking()
.Where(q => winnerRowIds.Contains(q.PurchaseEvaluationSupplierId))
.SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m;
if (winnerQuoteTotal <= 0)
missing.Add("Đơn vị được chọn chưa có giá chào thầu");
missing.Add("Đơn vị được chọn chưa có giá chào thầu");
}
// [S61 Mig 50] Schema ngân sách mới — điều kiện (3) = "Ngân sách - kỳ này"
// (BudgetPeriodAmount, drafter nhập). FE PeDetailTabs missingForApproval
@ -881,14 +879,14 @@ public class PurchaseEvaluationWorkflowService(
throw new ForbiddenException(
"Chỉ CCM (Kiểm soát Chi phí) được duyệt done miễn CEO.");
var winnerSupplierRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.IsWinner) // [Mig 58] multi-winner
.Select(s => s.Id)
.ToListAsync(ct);
var winnerQuoteTotal = winnerSupplierRowIds.Count == 0 ? 0m
: await db.PurchaseEvaluationQuotes.AsNoTracking()
.Where(q => winnerSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId))
.SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m;
// [multi-NCC A3(i)[3]] Giá gói = SUM ThanhTien báo giá ĐƯỢC CHỌN (Quote.IsSelected =
// nguồn-sự-thật) toàn phiếu. CCM duyệt done miễn CEO chỉ khi giá gói < ngưỡng (strict).
var winnerQuoteTotal = await (
from q in db.PurchaseEvaluationQuotes.AsNoTracking()
join d in db.PurchaseEvaluationDetails.AsNoTracking()
on q.PurchaseEvaluationDetailId equals d.Id
where d.PurchaseEvaluationId == evaluation.Id && q.IsSelected
select (decimal?)q.ThanhTien).SumAsync(ct) ?? 0m;
if (winnerQuoteTotal >= ceoThreshold)
throw new ConflictException(