[CLAUDE] PurchaseEvaluation: CCM 3-fix — multi-winner sum + budget=0 hợp lệ + cấp KẾT THÚC quy trình (Mig 58, anh Kiệt FDC)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m10s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m10s
CCM (Lưu Trần / anh Kiệt FDC) — làm 3/4 mục (mục 3 "CCM sửa ngân sách" để riêng): 1. Multi-winner: chọn NHIỀU NTP/NCC/ĐVDV cùng trúng → winnerQuoteTotal = SUM ThanhTien MỌI đơn vị isWinner (vd 3 ông 37.8+9+10 = 56.8tr). Cột mới PurchaseEvaluationSupplier.IsWinner (Mig 58, backfill từ SelectedSupplierId). SelectWinner → multi-toggle SupplierIds[]. FE: bảng so sánh tick nhiều ô + dòng "Tổng chi phí đã chọn"; mọi guard/checklist/computeGiaChaoThau/tạo-HĐ đổi sang isWinner (selectedSupplierId = null khi liên-danh ≥2). 2. Ngân sách = 0 hợp lệ: nới submit-guard + 3 validator (chỉ chặn khi NULL, số 0 cố ý đi qua; fallback ProInitial chỉ khi null). Mirror BE + FE. 3. Cấp KẾT THÚC quy trình: ApprovalWorkflowLevel.AllowApproverFinalize (Mig 58) — admin bật per-cấp trong Workflow Designer; duyệt tới cấp đó TỰ DaDuyet, bỏ qua CEO (AUTO theo cấu hình quy trình, không ô-tích runtime — anh Kiệt chốt). FE: banner cảnh báo + bắt chọn giá chốt. Mig 58 AddPeMultiWinnerAndLevelFinalize (2 AddColumn bool + backfill IsWinner, additive/reversible). Test 377→395 (+PeMultiWinnerTests +PeApproverFinalizeTests +budget=0; auto-model gọn bớt 2 test bypass/opt-out). FE 2 app SHA-mirror. Build sạch BE + 2 FE local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,231 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.PurchaseEvaluations;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Master;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
using SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Application;
|
||||
|
||||
// [Mig 58 2026-06-24 — CCM/anh Kiệt FDC — test-before/after] WinnerQuoteTotal multi-winner.
|
||||
// item 1: winnerQuoteTotal = SUM(báo giá ThanhTien) MỌI supplier-row IsWinner — KHÔNG còn
|
||||
// theo SelectedSupplierId (nguồn-sự-thật mới = IsWinner). Verify qua GetDetail read DTO
|
||||
// (GetPurchaseEvaluationQueryHandler → PurchaseEvaluationDetailBundleDto.WinnerQuoteTotal,
|
||||
// PurchaseEvaluationFeatures.cs ~1090-1097): build winnerSupplierRowIds = Suppliers IsWinner
|
||||
// → SUM Details.Quotes thuộc các row đó.
|
||||
//
|
||||
// Harness mirror PeWorkItemBudgetTests (IdentityFixture + SQLite + GetPurchaseEvaluationQuery
|
||||
// stack). KHÔNG touch production — test theo CODE đã land (S34). Query handler 3-dep
|
||||
// (db + UserManager + ICurrentUser); dùng Admin để bỏ qua authz scope.
|
||||
public class PeMultiWinnerTests
|
||||
{
|
||||
private sealed class FakeCurrentUser : ICurrentUser
|
||||
{
|
||||
public Guid? UserId { get; init; } = Guid.NewGuid();
|
||||
public string? Email { get; init; } = "actor@test.local";
|
||||
public string? FullName { get; init; } = "Actor Test";
|
||||
public IReadOnlyList<string> Roles { get; init; } = Array.Empty<string>();
|
||||
public bool IsAuthenticated => UserId is not null;
|
||||
}
|
||||
|
||||
private static FakeCurrentUser AsRoles(params string[] roles) => new() { Roles = roles };
|
||||
|
||||
private static GetPurchaseEvaluationQueryHandler BuildQueryHandler(
|
||||
IdentityFixture fix, TestApplicationDbContext db, ICurrentUser currentUser)
|
||||
=> new(db, fix.Services.GetRequiredService<UserManager<User>>(), currentUser);
|
||||
|
||||
private static async Task<PurchaseEvaluation> SeedPeAsync(TestApplicationDbContext db, string code = "PE-MW")
|
||||
{
|
||||
var pe = new PurchaseEvaluation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = PurchaseEvaluationType.DuyetNcc,
|
||||
Phase = PurchaseEvaluationPhase.DangSoanThao,
|
||||
MaPhieu = code,
|
||||
TenGoiThau = "Gói thầu multi-winner",
|
||||
ProjectId = Guid.NewGuid(),
|
||||
DrafterUserId = Guid.NewGuid(),
|
||||
};
|
||||
db.PurchaseEvaluations.Add(pe);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return pe;
|
||||
}
|
||||
|
||||
// 1 detail dùng chung cho mọi NCC (mỗi NCC 1 quote thuộc detail này).
|
||||
private static async Task<Guid> SeedDetailAsync(TestApplicationDbContext db, PurchaseEvaluation pe)
|
||||
{
|
||||
var detail = new PurchaseEvaluationDetail
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
GroupCode = "A.I",
|
||||
GroupName = "Bê tông",
|
||||
NoiDung = "Concrete M100",
|
||||
Order = 0,
|
||||
};
|
||||
db.PurchaseEvaluationDetails.Add(detail);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return detail.Id;
|
||||
}
|
||||
|
||||
// 1 NCC tham gia (master + PES row) + 1 quote ThanhTien thuộc detail. isWinner pin
|
||||
// IsWinner trên PES row. Return supplierId master.
|
||||
private static async Task<Guid> SeedSupplierWithQuoteAsync(
|
||||
TestApplicationDbContext db, PurchaseEvaluation pe, Guid detailId,
|
||||
string code, decimal thanhTien, bool isWinner, int order)
|
||||
{
|
||||
var supplier = new Supplier { Id = Guid.NewGuid(), Code = code, Name = "NCC " + code };
|
||||
db.Suppliers.Add(supplier);
|
||||
var pes = new PurchaseEvaluationSupplier
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PurchaseEvaluationId = pe.Id,
|
||||
SupplierId = supplier.Id,
|
||||
Order = order,
|
||||
IsWinner = isWinner,
|
||||
};
|
||||
db.PurchaseEvaluationSuppliers.Add(pes);
|
||||
db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PurchaseEvaluationDetailId = detailId,
|
||||
PurchaseEvaluationSupplierId = pes.Id,
|
||||
ThanhTien = thanhTien,
|
||||
});
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return supplier.Id;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WinnerQuoteTotal_ThreeWinners_SumsAllThreeThanhTien()
|
||||
{
|
||||
// ⭐ TRỤC CỐT LÕI item 1: 3 NCC cùng trúng (IsWinner=true) → WinnerQuoteTotal =
|
||||
// SUM cả 3 báo giá (37.8 + 9 + 10 = 56.8tr), KHÔNG chỉ 1 đơn vị.
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-MW-3WIN");
|
||||
var detailId = await SeedDetailAsync(db, pe);
|
||||
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-A", 37_800_000m, isWinner: true, order: 0);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-B", 9_000_000m, isWinner: true, order: 1);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-C", 10_000_000m, isWinner: true, order: 2);
|
||||
|
||||
var handler = BuildQueryHandler(fix, db, AsRoles(AppRoles.Admin));
|
||||
var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);
|
||||
|
||||
bundle.WinnerQuoteTotal.Should().Be(56_800_000m, "tổng giá gói = SUM báo giá MỌI đơn vị IsWinner");
|
||||
bundle.SelectedSupplierId.Should().BeNull("≥2 winner → SelectedSupplierId không sync 1 đơn vị");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WinnerQuoteTotal_ExcludesNonWinnerSuppliers()
|
||||
{
|
||||
// 4 NCC tham gia, chỉ 2 IsWinner=true → WinnerQuoteTotal CHỈ tính 2 winner
|
||||
// (100 + 50 = 150tr), bỏ qua 2 NCC không trúng (200 + 300tr KHÔNG cộng).
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-MW-EXCL");
|
||||
var detailId = await SeedDetailAsync(db, pe);
|
||||
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-W1", 100_000_000m, isWinner: true, order: 0);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-W2", 50_000_000m, isWinner: true, order: 1);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-L1", 200_000_000m, isWinner: false, order: 2);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-L2", 300_000_000m, isWinner: false, order: 3);
|
||||
|
||||
var handler = BuildQueryHandler(fix, db, AsRoles(AppRoles.Admin));
|
||||
var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);
|
||||
|
||||
bundle.WinnerQuoteTotal.Should().Be(150_000_000m, "chỉ SUM winner — NCC không trúng bị loại");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WinnerQuoteTotal_SingleWinner_EqualsThatSupplierQuote_SelectedSupplierIdSet()
|
||||
{
|
||||
// 1 winner (single-winner backward compat) → WinnerQuoteTotal = đúng báo giá đơn
|
||||
// vị đó + SelectedSupplierId sync = winner (handler select-winner set khi count==1).
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-MW-1WIN");
|
||||
var detailId = await SeedDetailAsync(db, pe);
|
||||
|
||||
var winner = await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-SOLO", 75_000_000m, isWinner: true, order: 0);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-OTHER", 80_000_000m, isWinner: false, order: 1);
|
||||
// Mô phỏng handler select-winner sync SelectedSupplierId khi đúng 1 winner.
|
||||
pe.SelectedSupplierId = winner;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var handler = BuildQueryHandler(fix, db, AsRoles(AppRoles.Admin));
|
||||
var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);
|
||||
|
||||
bundle.WinnerQuoteTotal.Should().Be(75_000_000m, "1 winner → tổng = đúng báo giá đơn vị đó");
|
||||
bundle.SelectedSupplierId.Should().Be(winner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WinnerQuoteTotal_NoWinner_IsZero()
|
||||
{
|
||||
// Chưa chọn winner nào (mọi PES IsWinner=false) → WinnerQuoteTotal = 0 (KHÔNG
|
||||
// lẫn báo giá các NCC tham gia chưa-trúng).
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-MW-NONE");
|
||||
var detailId = await SeedDetailAsync(db, pe);
|
||||
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-X", 40_000_000m, isWinner: false, order: 0);
|
||||
await SeedSupplierWithQuoteAsync(db, pe, detailId, "NCC-Y", 60_000_000m, isWinner: false, order: 1);
|
||||
|
||||
var handler = BuildQueryHandler(fix, db, AsRoles(AppRoles.Admin));
|
||||
var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);
|
||||
|
||||
bundle.WinnerQuoteTotal.Should().Be(0m, "0 winner → tổng gói = 0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WinnerQuoteTotal_MultipleQuotesPerWinner_SumsAllRows()
|
||||
{
|
||||
// 1 winner nhưng có 2 detail (2 dòng báo giá thuộc cùng PES winner) → SUM cả 2
|
||||
// dòng (20 + 30 = 50tr). Verify SelectMany Details.Quotes gộp mọi dòng của winner-row.
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-MW-MULTQ");
|
||||
|
||||
// 2 detail rời.
|
||||
var detail1 = await SeedDetailAsync(db, pe);
|
||||
var detail2 = new PurchaseEvaluationDetail
|
||||
{
|
||||
Id = Guid.NewGuid(), PurchaseEvaluationId = pe.Id,
|
||||
GroupCode = "A.II", GroupName = "Thép", NoiDung = "Steel D10", Order = 1,
|
||||
};
|
||||
db.PurchaseEvaluationDetails.Add(detail2);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// 1 winner PES + 2 quote (mỗi detail 1 dòng).
|
||||
var supplier = new Supplier { Id = Guid.NewGuid(), Code = "NCC-2Q", Name = "NCC 2 dòng" };
|
||||
db.Suppliers.Add(supplier);
|
||||
var pes = new PurchaseEvaluationSupplier
|
||||
{
|
||||
Id = Guid.NewGuid(), PurchaseEvaluationId = pe.Id, SupplierId = supplier.Id,
|
||||
Order = 0, IsWinner = true,
|
||||
};
|
||||
db.PurchaseEvaluationSuppliers.Add(pes);
|
||||
db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
|
||||
{
|
||||
Id = Guid.NewGuid(), PurchaseEvaluationDetailId = detail1,
|
||||
PurchaseEvaluationSupplierId = pes.Id, ThanhTien = 20_000_000m,
|
||||
});
|
||||
db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
|
||||
{
|
||||
Id = Guid.NewGuid(), PurchaseEvaluationDetailId = detail2.Id,
|
||||
PurchaseEvaluationSupplierId = pes.Id, ThanhTien = 30_000_000m,
|
||||
});
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var handler = BuildQueryHandler(fix, db, AsRoles(AppRoles.Admin));
|
||||
var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None);
|
||||
|
||||
bundle.WinnerQuoteTotal.Should().Be(50_000_000m, "SUM mọi dòng báo giá của winner-row qua nhiều detail");
|
||||
}
|
||||
}
|
||||
@ -10,15 +10,21 @@ using SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Application;
|
||||
|
||||
// [anh Kiệt FDC C5 — test-after] SelectPurchaseEvaluationWinnerCommand: NCC winner
|
||||
// nullable hoá. Command record `(Guid PurchaseEvaluationId, Guid? SupplierId)`.
|
||||
// Handler (PurchaseEvaluationDetailFeatures.cs ~388-438) 2 nhánh theo SupplierId:
|
||||
// • `SupplierId is Guid x` → CHỌN/ĐỔI (logic cũ giữ 100%): NotFound nếu supplier
|
||||
// không có ở master + Conflict nếu supplier KHÔNG thuộc participating-list của
|
||||
// phiếu (PurchaseEvaluationSuppliers) + set entity.SelectedSupplierId = x.
|
||||
// • `SupplierId == null` → BỎ CHỌN (clear về none): set SelectedSupplierId = null,
|
||||
// BỎ QUA validate participating-list (nhánh MỚI — trục test quan trọng nhất).
|
||||
// Mọi nhánh ghi 1 Changelog (Header/Update) — Summary phân theo null vs có-giá-trị.
|
||||
// [Mig 58 2026-06-24 — CCM/anh Kiệt FDC — test-after] SelectPurchaseEvaluationWinnerCommand
|
||||
// MULTI-WINNER. Command record ĐỔI `(Guid PurchaseEvaluationId, Guid? SupplierId)` →
|
||||
// `(Guid PurchaseEvaluationId, IReadOnlyList<Guid> SupplierIds)`.
|
||||
// Handler (PurchaseEvaluationDetailFeatures.cs ~404-447):
|
||||
// • Load MỌI supplier-row của phiếu (tracking) → set IsWinner = (row.SupplierId ∈
|
||||
// selectedIds), false phần còn lại. IsWinner trên supplier-row = nguồn-sự-thật mới.
|
||||
// • selectedIds rỗng [] = BỎ CHỌN tất cả (IsWinner=false hết) — thay null cũ (anh Kiệt C5).
|
||||
// • Mỗi id chọn PHẢI thuộc participating-list (PurchaseEvaluationSuppliers) → Conflict.
|
||||
// • Distinct id (FE có thể gửi trùng).
|
||||
// • Sync SelectedSupplierId: đúng 1 winner → đơn vị đó (backward compat luồng tạo HĐ +
|
||||
// DTO cũ); ≥2 HOẶC 0 → null.
|
||||
// • KHÔNG còn check master Suppliers existence (logic mới chỉ validate participating-list)
|
||||
// — NotFound master-existence bỏ; chỉ Conflict khi id không thuộc list của phiếu.
|
||||
// Mọi nhánh ghi 1 Changelog (Header/Update) — Summary "Bỏ chọn đơn vị NCC/TP trúng thầu"
|
||||
// (count==0) vs "Chọn {n} đơn vị NCC/TP trúng thầu".
|
||||
//
|
||||
// Handler 2-dep (db + ICurrentUser), KHÔNG cần UserManager → SqliteDbFixture đủ nhẹ
|
||||
// (mirror DepartmentTreeTests). ICurrentUser fake cứng để Changelog ghi UserId/Name.
|
||||
@ -67,74 +73,97 @@ public class PeSelectWinnerClearTests
|
||||
}
|
||||
|
||||
// Gắn supplier vào participating-list (PurchaseEvaluationSuppliers) của phiếu.
|
||||
private static async Task AddParticipatingAsync(
|
||||
// Return PES row Id để assert IsWinner.
|
||||
private static async Task<Guid> AddParticipatingAsync(
|
||||
TestApplicationDbContext db, Guid peId, Guid supplierId, int order = 0)
|
||||
{
|
||||
db.PurchaseEvaluationSuppliers.Add(new PurchaseEvaluationSupplier
|
||||
var row = new PurchaseEvaluationSupplier
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PurchaseEvaluationId = peId,
|
||||
SupplierId = supplierId,
|
||||
Order = order,
|
||||
});
|
||||
};
|
||||
db.PurchaseEvaluationSuppliers.Add(row);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return row.Id;
|
||||
}
|
||||
|
||||
// Re-read IsWinner cho 1 supplier-row (theo SupplierId) — AsNoTracking để né cache.
|
||||
private static async Task<bool> IsWinnerAsync(
|
||||
TestApplicationDbContext db, Guid peId, Guid supplierId)
|
||||
=> await db.PurchaseEvaluationSuppliers.AsNoTracking()
|
||||
.Where(s => s.PurchaseEvaluationId == peId && s.SupplierId == supplierId)
|
||||
.Select(s => s.IsWinner)
|
||||
.FirstAsync();
|
||||
|
||||
// ============================================================
|
||||
// 1. CLEAR branch (MỚI — trục quan trọng nhất)
|
||||
// 1. CLEAR branch ([] = bỏ chọn tất cả — trục quan trọng nhất)
|
||||
// ============================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_PhieuHasSelectedWinner_SetsSelectedSupplierIdNull()
|
||||
public async Task Clear_PhieuHasSelectedWinner_SetsSelectedSupplierIdNull_AllRowsNotWinner()
|
||||
{
|
||||
// ⭐ Phiếu đang có SelectedSupplierId = winner (s1) + s1 VẪN nằm trong
|
||||
// participating-list → gọi SupplierId=null → clear về null, KHÔNG ném.
|
||||
// ⭐ Phiếu đang có 1 winner (s1, IsWinner=true) + s1 trong participating-list →
|
||||
// gọi SupplierIds=[] → clear: SelectedSupplierId=null + s1.IsWinner=false.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-A");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: s1.Id);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
// Pre-set s1 = winner (mô phỏng phiếu đã chọn trước).
|
||||
var row = await db.PurchaseEvaluationSuppliers.FirstAsync(s => s.SupplierId == s1.Id);
|
||||
row.IsWinner = true;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: null);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: Array.Empty<Guid>());
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().BeNull("bỏ chọn = clear winner về none");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeFalse("clear → mọi supplier-row IsWinner=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_WinnerNotInParticipatingList_StillClears_NoConflict()
|
||||
public async Task Clear_EmptyList_AllRowsNotWinner_NoConflict()
|
||||
{
|
||||
// Trục cốt lõi nhánh clear: SKIP validate participating-list. Phiếu có
|
||||
// SelectedSupplierId = s1 (winner cũ) nhưng s1 KHÔNG còn trong list (vd bị
|
||||
// gỡ khỏi bảng so sánh). Clear vẫn phải thành công, KHÔNG ném Conflict/NotFound.
|
||||
// Trục cốt lõi nhánh clear: SKIP validate participating-list (list rỗng = không
|
||||
// có id cần kiểm). Phiếu có 2 NCC tham gia, s1 đang là winner → clear [] →
|
||||
// KHÔNG ném + cả 2 row IsWinner=false + SelectedSupplierId=null.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-GONE");
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-1");
|
||||
var s2 = await SeedSupplierAsync(db, "NCC-2");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: s1.Id);
|
||||
// CỐ Ý không AddParticipating → s1 không thuộc list của phiếu.
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
var r1 = await db.PurchaseEvaluationSuppliers.FirstAsync(s => s.SupplierId == s1.Id);
|
||||
r1.IsWinner = true;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: null);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: Array.Empty<Guid>());
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().NotThrowAsync("clear KHÔNG validate participating-list");
|
||||
await act.Should().NotThrowAsync("clear [] KHÔNG validate participating-list");
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().BeNull();
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeFalse();
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_PhieuAlreadyNull_IsIdempotent_StaysNull_NoThrow()
|
||||
{
|
||||
// Phiếu vốn-dĩ chưa chọn winner (SelectedSupplierId=null) → clear lần nữa =
|
||||
// idempotent, vẫn null, không ném. Empty participating-list cũng không cản.
|
||||
// Phiếu vốn-dĩ chưa chọn winner (SelectedSupplierId=null, không row IsWinner) →
|
||||
// clear [] lần nữa = idempotent, vẫn null, không ném. Empty list cũng không cản.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: null);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: Array.Empty<Guid>());
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().NotThrowAsync();
|
||||
@ -145,34 +174,35 @@ public class PeSelectWinnerClearTests
|
||||
[Fact]
|
||||
public async Task Clear_WritesChangelog_WithBoChonSummary()
|
||||
{
|
||||
// Nhánh clear vẫn ghi 1 Changelog Header/Update với Summary "Bỏ chọn NCC
|
||||
// trúng thầu" (phân biệt với "Chọn NCC trúng thầu" của nhánh select).
|
||||
// Nhánh clear vẫn ghi 1 Changelog Header/Update với Summary "Bỏ chọn đơn vị
|
||||
// NCC/TP trúng thầu" (phân biệt với "Chọn {n} đơn vị..." của nhánh select).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-A");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: s1.Id);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: null);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: Array.Empty<Guid>());
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var log = await db.PurchaseEvaluationChangelogs.AsNoTracking()
|
||||
.Where(c => c.PurchaseEvaluationId == pe.Id)
|
||||
.SingleAsync();
|
||||
log.Summary.Should().Be("Bỏ chọn NCC trúng thầu");
|
||||
log.Summary.Should().Be("Bỏ chọn đơn vị NCC/TP trúng thầu");
|
||||
log.EntityType.Should().Be(PurchaseEvaluationEntityType.Header);
|
||||
log.Action.Should().Be(ChangelogAction.Update);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. SELECT branch (regression — logic cũ giữ nguyên 100%)
|
||||
// 2. SELECT branch — 1 winner (backward compat single-winner)
|
||||
// ============================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Select_SupplierInParticipatingList_SetsSelectedSupplierId()
|
||||
public async Task Select_OneSupplierInList_SetsSelectedSupplierId_RowIsWinner()
|
||||
{
|
||||
// Happy-path cũ: supplier hợp lệ (tồn tại master + thuộc participating-list)
|
||||
// → set SelectedSupplierId = đúng id.
|
||||
// Happy-path single-winner: 1 supplier hợp lệ (thuộc participating-list) →
|
||||
// SelectedSupplierId = đúng id (count==1 sync) + row IsWinner=true.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-WIN");
|
||||
@ -180,18 +210,19 @@ public class PeSelectWinnerClearTests
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: s1.Id);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id });
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().Be(s1.Id);
|
||||
reloaded.SelectedSupplierId.Should().Be(s1.Id, "đúng 1 winner → SelectedSupplierId sync");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_ChangeWinner_FromS1ToS2_BothInList_UpdatesToS2()
|
||||
public async Task Select_ChangeWinner_FromS1ToS2_BothInList_UpdatesToS2_OnlyS2IsWinner()
|
||||
{
|
||||
// Đổi winner: phiếu đang chọn s1 → chọn s2 (cả 2 trong list) → SelectedSupplierId
|
||||
// chuyển s2 (chứng minh nhánh select overwrite, không chỉ set-from-null).
|
||||
// Đổi winner: phiếu đang chọn s1 → chọn [s2] (cả 2 trong list) → SelectedSupplierId
|
||||
// chuyển s2 + s2.IsWinner=true + s1.IsWinner=false (set false phần còn lại).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-1");
|
||||
@ -199,68 +230,217 @@ public class PeSelectWinnerClearTests
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: s1.Id);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
// s1 đang là winner.
|
||||
var r1 = await db.PurchaseEvaluationSuppliers.FirstAsync(s => s.SupplierId == s1.Id);
|
||||
r1.IsWinner = true;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: s2.Id);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s2.Id });
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().Be(s2.Id);
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeTrue("s2 là winner mới");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeFalse("winner cũ s1 bị set false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_SupplierNotInParticipatingList_ThrowsConflict_NoMutate()
|
||||
{
|
||||
// Supplier tồn tại ở master NHƯNG chưa được thêm vào phiếu → Conflict.
|
||||
// entity.SelectedSupplierId giữ nguyên (throw TRƯỚC khi gán + SaveChanges).
|
||||
// Supplier không thuộc participating-list của phiếu → Conflict. IsWinner +
|
||||
// SelectedSupplierId giữ nguyên (throw TRƯỚC khi set + SaveChanges).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-IN"); // trong list
|
||||
var outsider = await SeedSupplierAsync(db, "NCC-OUT"); // không trong list
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: s1.Id);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
var r1 = await db.PurchaseEvaluationSuppliers.FirstAsync(s => s.SupplierId == s1.Id);
|
||||
r1.IsWinner = true;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: outsider.Id);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { outsider.Id });
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>()
|
||||
.WithMessage("*NCC chưa được thêm vào phiếu*");
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().Be(s1.Id, "Conflict TRƯỚC gán → winner cũ s1 giữ nguyên");
|
||||
reloaded.SelectedSupplierId.Should().Be(s1.Id, "Conflict TRƯỚC mutate → winner cũ s1 giữ nguyên");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue("Conflict → IsWinner s1 không bị set false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_SupplierNotInMaster_ThrowsNotFound()
|
||||
public async Task Select_OneIdInListOneOutside_ThrowsConflict_NoPartialMutate()
|
||||
{
|
||||
// Supplier Guid không tồn tại trong Suppliers master → NotFound (check master
|
||||
// existence TRƯỚC participating-list).
|
||||
// Multi-select với 1 id ngoài list (s1 trong, outsider ngoài) → Conflict (mỗi id
|
||||
// phải thuộc list). KHÔNG partial-write: s1.IsWinner KHÔNG bị set true dù hợp lệ
|
||||
// (validate TRƯỚC khi set IsWinner cho toàn bộ rows).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-OK");
|
||||
var outsider = await SeedSupplierAsync(db, "NCC-FORGE");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierId: Guid.NewGuid());
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, outsider.Id });
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
await act.Should().ThrowAsync<ConflictException>()
|
||||
.WithMessage("*NCC chưa được thêm vào phiếu*");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeFalse("Conflict → KHÔNG set winner cho id hợp lệ trong cùng lệnh");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. PE existence — chung cả 2 nhánh (check TRƯỚC khi rẽ nhánh SupplierId)
|
||||
// 3. SELECT branch — MULTI winner (≥2 → joint winner, SelectedSupplierId=null)
|
||||
// [TASK 2.1] regression cho hành vi multi-winner mới (Mig 58).
|
||||
// ============================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Select_TwoSuppliers_BothIsWinner_SelectedSupplierIdNull()
|
||||
{
|
||||
// Chọn 2 đơn vị cùng trúng → cả 2 IsWinner=true, SelectedSupplierId=null (≥2 →
|
||||
// không còn "1 winner" duy nhất). Trục multi-winner cốt lõi.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-J1");
|
||||
var s2 = await SeedSupplierAsync(db, "NCC-J2");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, s2.Id });
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().BeNull("≥2 winner → không sync 1 SelectedSupplierId");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue();
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_ThreeOfFour_ThreeIsWinner_NonSelectedIsFalse_SelectedSupplierIdNull()
|
||||
{
|
||||
// 4 NCC tham gia, chọn 3 → đúng 3 row IsWinner=true, NCC thứ 4 IsWinner=false,
|
||||
// SelectedSupplierId=null. Verify "set false phần còn lại" với nhiều row.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-Q1");
|
||||
var s2 = await SeedSupplierAsync(db, "NCC-Q2");
|
||||
var s3 = await SeedSupplierAsync(db, "NCC-Q3");
|
||||
var s4 = await SeedSupplierAsync(db, "NCC-Q4");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
await AddParticipatingAsync(db, pe.Id, s3.Id, order: 2);
|
||||
await AddParticipatingAsync(db, pe.Id, s4.Id, order: 3);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, s2.Id, s3.Id });
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().BeNull();
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue();
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeTrue();
|
||||
(await IsWinnerAsync(db, pe.Id, s3.Id)).Should().BeTrue();
|
||||
(await IsWinnerAsync(db, pe.Id, s4.Id)).Should().BeFalse("NCC không được chọn → IsWinner=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_DuplicateIds_Deduped_TreatedAsSingleWinner_SelectedSupplierIdSet()
|
||||
{
|
||||
// FE gửi trùng id [s1, s1] → Distinct → count==1 → đối xử single-winner:
|
||||
// s1.IsWinner=true + SelectedSupplierId=s1 (KHÔNG bị coi là ≥2 do trùng).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-DUP");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, s1.Id });
|
||||
await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().Be(s1.Id, "trùng id Distinct → count==1 → sync SelectedSupplierId");
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_MultiThenClear_RoundTrip_AllWinnersReset()
|
||||
{
|
||||
// Round-trip: chọn 2 (joint) → rồi clear [] → cả 2 IsWinner về false +
|
||||
// SelectedSupplierId=null. Chứng minh clear reset đúng kể cả từ trạng thái multi.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-R1");
|
||||
var s2 = await SeedSupplierAsync(db, "NCC-R2");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
await handler.Handle(
|
||||
new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, s2.Id }),
|
||||
CancellationToken.None);
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeTrue("sau chọn 2");
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeTrue();
|
||||
|
||||
await handler.Handle(
|
||||
new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: Array.Empty<Guid>()),
|
||||
CancellationToken.None);
|
||||
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.SelectedSupplierId.Should().BeNull();
|
||||
(await IsWinnerAsync(db, pe.Id, s1.Id)).Should().BeFalse("clear sau multi → false hết");
|
||||
(await IsWinnerAsync(db, pe.Id, s2.Id)).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Select_MultiWinner_WritesChangelog_WithChonNSummary()
|
||||
{
|
||||
// Multi-winner ghi Changelog Summary "Chọn {n} đơn vị NCC/TP trúng thầu" (n = số
|
||||
// đơn vị chọn). Verify count chính xác trong message.
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-CL1");
|
||||
var s2 = await SeedSupplierAsync(db, "NCC-CL2");
|
||||
var pe = await SeedPeAsync(db, selectedSupplierId: null);
|
||||
await AddParticipatingAsync(db, pe.Id, s1.Id, order: 0);
|
||||
await AddParticipatingAsync(db, pe.Id, s2.Id, order: 1);
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
await handler.Handle(
|
||||
new SelectPurchaseEvaluationWinnerCommand(pe.Id, SupplierIds: new[] { s1.Id, s2.Id }),
|
||||
CancellationToken.None);
|
||||
|
||||
var log = await db.PurchaseEvaluationChangelogs.AsNoTracking()
|
||||
.Where(c => c.PurchaseEvaluationId == pe.Id)
|
||||
.SingleAsync();
|
||||
log.Summary.Should().Be("Chọn 2 đơn vị NCC/TP trúng thầu");
|
||||
log.EntityType.Should().Be(PurchaseEvaluationEntityType.Header);
|
||||
log.Action.Should().Be(ChangelogAction.Update);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 4. PE existence — chung mọi nhánh (check TRƯỚC khi rẽ nhánh SupplierIds)
|
||||
// ============================================================
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownPe_Clear_ThrowsNotFound_BeforeBranch()
|
||||
{
|
||||
// PE không tồn tại → NotFound("PurchaseEvaluation") ngay đầu handler, TRƯỚC
|
||||
// khi xét SupplierId (kể cả nhánh clear null cũng không bỏ qua existence).
|
||||
// khi xét SupplierIds (kể cả nhánh clear [] cũng không bỏ qua existence).
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(Guid.NewGuid(), SupplierId: null);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(Guid.NewGuid(), SupplierIds: Array.Empty<Guid>());
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
@ -274,7 +454,7 @@ public class PeSelectWinnerClearTests
|
||||
var s1 = await SeedSupplierAsync(db, "NCC-A");
|
||||
var handler = BuildHandler(db);
|
||||
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(Guid.NewGuid(), SupplierId: s1.Id);
|
||||
var cmd = new SelectPurchaseEvaluationWinnerCommand(Guid.NewGuid(), SupplierIds: new[] { s1.Id });
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
|
||||
@ -857,13 +857,25 @@ public class PeWorkItemBudgetTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustBudget_Validator_BudgetPeriodZero_FailsValidation()
|
||||
public void AdjustBudget_Validator_BudgetPeriodZero_PassesValidation()
|
||||
{
|
||||
// GreaterThan(0) when HasValue → 0 không hợp lệ.
|
||||
// [Mig 58 item 2 — SPEC CHANGE] GreaterThanOrEqualTo(0) when HasValue → 0 HỢP LỆ
|
||||
// (trước GreaterThan(0) chặn 0). SSG ngân sách 0 vẫn lưu/gửi duyệt được (anh Kiệt FDC).
|
||||
var validator = new AdjustPurchaseEvaluationBudgetCommandValidator();
|
||||
var result = validator.Validate(new AdjustPurchaseEvaluationBudgetCommand(Guid.NewGuid(), 0m, null));
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.IsValid.Should().BeTrue("ngân sách kỳ này = 0 hợp lệ sau Mig 58");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustBudget_Validator_BudgetPeriodNegative_FailsValidation()
|
||||
{
|
||||
// [Mig 58 item 2] Boundary: 0 hợp lệ NHƯNG ÂM vẫn KHÔNG (GreaterThanOrEqualTo(0)
|
||||
// = >= 0, loại số âm). Chứng minh nới chỉ tới 0, không mở cho âm.
|
||||
var validator = new AdjustPurchaseEvaluationBudgetCommandValidator();
|
||||
var result = validator.Validate(new AdjustPurchaseEvaluationBudgetCommand(Guid.NewGuid(), -1m, null));
|
||||
|
||||
result.IsValid.Should().BeFalse("ngân sách kỳ này ÂM không hợp lệ");
|
||||
result.Errors.Should().Contain(e =>
|
||||
e.PropertyName == nameof(AdjustPurchaseEvaluationBudgetCommand.BudgetPeriodAmount));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user