using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// Phase 5C 最后复核:证明 UoW 的「INSERT…ON DUPLICATE KEY(noop) 首建 bootstrap」**不绕过 G4/G5**。 /// 守卫在 LockAndLoad(只读既有行) 之后、Write(含 bootstrap) 之前运行 → 缺行时 G4/G5 先命中、0 写; /// 仅「允许首次创建的 TransType(如 rct-po-ins 入库)」在守卫通过后才 bootstrap。选项 A(正确,不污染存在性语义)。 /// public class InventoryBootstrapGuardTests { private const long Tenant = 797403760988229L; private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1") => new InventoryTransactionCommand { TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, Seq = 1, TransType = tt, ChangeQty = qty, UmConversion = 1m, ItemNum = item, Location = loc, User = "t", }; private static InventoryTransactionService Svc(FakeInventoryDatabase db) => new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db)); // Test 1:净出库 + InvMaster 不存在 → 首命中 G4(InvMasterMissing),非 G9;且不 bootstrap [Fact] public async Task NetOutbound_NoInvMaster_HitsG4_NotG9_NoBootstrap() { var db = new FakeInventoryDatabase(); db.SeedMaster(Tenant, "8010", "IT-1", "L1"); // 主数据在,无库存行 var r = await Svc(db).PostAsync(new[] { Cmd("iss-x", -10m) }); Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome); Assert.Equal(InventoryGuardCode.InvMasterMissing, r.GuardErrors[0].Code); // 首命中 G4 Assert.Equal(0, db.InvMaster.Count); // 未 bootstrap 占位行 Assert.Equal(0, db.LocationDetail.Count); Assert.Equal(0, db.TotalPersisted); } // Test 2:净出库 + InvMaster 存在但 LocationDetail 不存在 → 首命中 G5(LocationDetailMissing);且不 bootstrap 明细 [Fact] public async Task NetOutbound_InvMasterOnly_NoLocationDetail_HitsG5_NoBootstrap() { var db = new FakeInventoryDatabase(); db.SeedMaster(Tenant, "8010", "IT-1", "L1"); db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")] = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 100m, Assay = 100m, FreezeQty = 100m }; var r = await Svc(db).PostAsync(new[] { Cmd("iss-x", -10m) }); Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome); Assert.Equal(InventoryGuardCode.LocationDetailMissing, r.GuardErrors[0].Code); // 首命中 G5 Assert.Equal(0, db.LocationDetail.Count); // 未 bootstrap 明细行 Assert.Equal(1, db.InvMaster.Count); // 既有 InvMaster 未被改动/新增 } // Test 3:rct-po-ins(允许首次创建)+ 两表均不存在 → 允许 bootstrap 并成功 [Fact] public async Task RctPoIns_BothMissing_BootstrapAndSucceed() { var db = new FakeInventoryDatabase(); db.SeedMaster(Tenant, "8010", "IT-1", "L1"); var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 100m) }); Assert.Equal(InventoryPostingOutcome.Success, r.Outcome); Assert.Equal(1, db.InvMaster.Count); // 首建 Assert.Equal(1, db.LocationDetail.Count); Assert.Equal(100m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].QtyOnHand); } }