[CLAUDE] Supplier: import v2 — Mig 64 publish/draft + dedup-MST + template + file mẫu
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m14s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m14s
- Mig 64 AddSupplierPublishState: +IsPublic (draft/public); backfill 22 prod->public; filtered-unique doi [Code]<>'' (cho phep nhieu nhap Code=''); no new table (89). - Import v2: dedup MST-primary + Code-backstop (chong 500 unique-violation); re-bake 30 token header THAT byte-exact (LayoutValid khop file that); nhap Ma NCC per-row (=Code); thieu Ma NCC->nhap (IsPublic=false); MST thieu->canh bao mem (MstMissing). - PublishSupplierCommand rieng (ne #73 clobber); GET /suppliers/import/template (BE-gen xlsx 30-col). - FE 2-app SHA-mirror: badge Public/Nhap; nut Cong bo; filter; nut Tai file mau; cot Ma editable; canh bao MST. Picker PE + tao HD loc published=true (an nhap -> ma HD khong dinh Code rong). CreateSupplier set IsPublic=true. - authz D3: import/preview/confirm/template/publish = Policy Suppliers.Update (khop FE PermissionGuard, het 403). - Tests +19 (477 PASS): dedup T1-T7, publish-guard, list-filter, all-or-nothing, LayoutValid. Fix null-Code NRE (path R4 loi). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,155 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Master.Suppliers.Commands.PublishSupplier;
|
||||
using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier;
|
||||
using SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers;
|
||||
using SolutionErp.Domain.Master;
|
||||
using SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Application;
|
||||
|
||||
// ============================================================================
|
||||
// Supplier import v2 (S113) — PublishSupplierCommand + ListSuppliers/GetSupplier IsPublic.
|
||||
// Publish-gate = SECURITY/correctness (cổng công bố NCC) → test-before-merge (docs/rules.md §7).
|
||||
// Test theo CODE trên đĩa (S34) — handlers chỉ cần IApplicationDbContext → new trực tiếp với fix.Db.
|
||||
//
|
||||
// 🔑 PublishSupplierCommand (KHÔNG dùng UpdateSupplier, tránh #73 clobber):
|
||||
// - MaNcc gửi (non-null): set Code=Trim; unique-check `x.Code == newCode && x.Id != id` (khi len>0
|
||||
// & khác Code cũ) → ConflictException nếu trùng.
|
||||
// - Gate D2: Publish=true ∧ Code rỗng → ConflictException "Cần Mã NCC...".
|
||||
// - IsPublic = Publish.
|
||||
//
|
||||
// ⚠️ Unique-check publish dùng `x.Code == newCode` (DB collation), KHÁC import service
|
||||
// (OrdinalIgnoreCase in-mem). SQLite BINARY → chỉ bắt trùng ĐÚNG hoa/thường. Prod SQL Server
|
||||
// CI-collation mới bắt CI-dup. Test T9b dùng trùng ĐÚNG-case (deterministic trên SQLite).
|
||||
// (Observation, KHÔNG phải bug — prod collation CI xử lý; flag để biết asymmetry.)
|
||||
// ============================================================================
|
||||
public class SupplierPublishAndListTests
|
||||
{
|
||||
private static Supplier NewSupplier(string code, string name, bool isPublic,
|
||||
SupplierType type = SupplierType.NhaCungCap) =>
|
||||
new() { Id = Guid.NewGuid(), Code = code, Name = name, Type = type, IsPublic = isPublic };
|
||||
|
||||
// ---- T9 — publish có Mã NCC → set Code + IsPublic=true ----
|
||||
[Fact]
|
||||
public async Task PublishSupplier_WithMaNcc_SetsCode_AndIsPublicTrue()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var draft = NewSupplier("", "Draft NCC", isPublic: false); // nháp, chưa có Mã NCC
|
||||
db.Suppliers.Add(draft);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await new PublishSupplierCommandHandler(db)
|
||||
.Handle(new PublishSupplierCommand(draft.Id, MaNcc: "NEW01", Publish: true), CancellationToken.None);
|
||||
|
||||
var reloaded = await db.Suppliers.SingleAsync(x => x.Id == draft.Id);
|
||||
reloaded.Code.Should().Be("NEW01", "MaNcc gửi → set Code");
|
||||
reloaded.IsPublic.Should().BeTrue("Publish=true + có Code → công bố");
|
||||
}
|
||||
|
||||
// ---- T9b — publish với Mã NCC trùng (đúng hoa/thường) NCC khác → ConflictException ----
|
||||
[Fact]
|
||||
public async Task PublishSupplier_MaNccCollidesExactCaseWithOther_ThrowsConflict_NoChange()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var other = NewSupplier("TAKEN", "Other", isPublic: true);
|
||||
var draft = NewSupplier("", "Draft", isPublic: false);
|
||||
db.Suppliers.AddRange(other, draft);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var act = async () => await new PublishSupplierCommandHandler(db)
|
||||
.Handle(new PublishSupplierCommand(draft.Id, MaNcc: "TAKEN", Publish: true), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>()
|
||||
.WithMessage("*đã tồn tại*", "Mã NCC trùng NCC khác → chặn (unique-check)");
|
||||
|
||||
var reloaded = await db.Suppliers.SingleAsync(x => x.Id == draft.Id);
|
||||
reloaded.Code.Should().Be("", "throw trước khi gán → draft KHÔNG đổi");
|
||||
reloaded.IsPublic.Should().BeFalse();
|
||||
}
|
||||
|
||||
// ---- T10 — publish khi Code rỗng (không gửi MaNcc) → ConflictException "Cần Mã NCC" ----
|
||||
[Fact]
|
||||
public async Task PublishSupplier_PublishTrue_ButCodeEmpty_ThrowsConflict_CanMaNcc()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var draft = NewSupplier("", "Draft NCC", isPublic: false);
|
||||
db.Suppliers.Add(draft);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var act = async () => await new PublishSupplierCommandHandler(db)
|
||||
.Handle(new PublishSupplierCommand(draft.Id, MaNcc: null, Publish: true), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>()
|
||||
.WithMessage("*Cần Mã NCC*", "gate D2: công bố bắt buộc có Mã NCC");
|
||||
|
||||
(await db.Suppliers.SingleAsync(x => x.Id == draft.Id)).IsPublic
|
||||
.Should().BeFalse("gate chặn → vẫn nháp");
|
||||
}
|
||||
|
||||
// ---- T11 — unpublish (Publish=false) LUÔN cho, IsPublic=false, giữ Code ----
|
||||
[Fact]
|
||||
public async Task PublishSupplier_Unpublish_AlwaysAllowed_SetsIsPublicFalse_KeepsCode()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var pub = NewSupplier("KEEP", "Public NCC", isPublic: true);
|
||||
db.Suppliers.Add(pub);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await new PublishSupplierCommandHandler(db)
|
||||
.Handle(new PublishSupplierCommand(pub.Id, MaNcc: null, Publish: false), CancellationToken.None);
|
||||
|
||||
var reloaded = await db.Suppliers.SingleAsync(x => x.Id == pub.Id);
|
||||
reloaded.IsPublic.Should().BeFalse("ẩn luôn được, KHÔNG cần Mã NCC");
|
||||
reloaded.Code.Should().Be("KEEP", "unpublish KHÔNG đụng Code");
|
||||
}
|
||||
|
||||
// ---- T12 — ListSuppliers Published filter: true / false / null=all ----
|
||||
[Fact]
|
||||
public async Task ListSuppliers_PublishedFilter_FiltersByIsPublic_NullReturnsAll()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
db.Suppliers.AddRange(
|
||||
NewSupplier("P1", "Pub 1", isPublic: true),
|
||||
NewSupplier("P2", "Pub 2", isPublic: true),
|
||||
NewSupplier("", "Draft 1", isPublic: false));
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var handler = new ListSuppliersQueryHandler(db);
|
||||
|
||||
var published = await handler.Handle(new ListSuppliersQuery(Published: true) { PageSize = 100 }, CancellationToken.None);
|
||||
published.Total.Should().Be(2, "Published=true → chỉ NCC đã công bố");
|
||||
published.Items.Should().OnlyContain(x => x.IsPublic);
|
||||
|
||||
var drafts = await handler.Handle(new ListSuppliersQuery(Published: false) { PageSize = 100 }, CancellationToken.None);
|
||||
drafts.Total.Should().Be(1, "Published=false → chỉ nháp");
|
||||
drafts.Items.Should().OnlyContain(x => !x.IsPublic);
|
||||
|
||||
var all = await handler.Handle(new ListSuppliersQuery(Published: null) { PageSize = 100 }, CancellationToken.None);
|
||||
all.Total.Should().Be(3, "null → tất cả (admin quản lý thấy cả nháp)");
|
||||
}
|
||||
|
||||
// ---- T13 — SupplierDto.IsPublic project đúng (GetSupplier single-item projection) ----
|
||||
[Fact]
|
||||
public async Task GetSupplier_ProjectsIsPublic_ForBothPublicAndDraft()
|
||||
{
|
||||
using var fix = new SqliteDbFixture();
|
||||
var db = fix.Db;
|
||||
var pub = NewSupplier("G1", "Public", isPublic: true);
|
||||
var draft = NewSupplier("", "Draft", isPublic: false);
|
||||
db.Suppliers.AddRange(pub, draft);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var handler = new GetSupplierQueryHandler(db);
|
||||
|
||||
(await handler.Handle(new GetSupplierQuery(pub.Id), CancellationToken.None)).IsPublic
|
||||
.Should().BeTrue("DTO project IsPublic=true cho NCC công bố");
|
||||
(await handler.Handle(new GetSupplierQuery(draft.Id), CancellationToken.None)).IsPublic
|
||||
.Should().BeFalse("DTO project IsPublic=false cho nháp");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user