| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>
- /// Phase 5C 最后复核:证明 UoW 的「INSERT…ON DUPLICATE KEY(noop) 首建 bootstrap」**不绕过 G4/G5**。
- /// 守卫在 LockAndLoad(只读既有行) 之后、Write(含 bootstrap) 之前运行 → 缺行时 G4/G5 先命中、0 写;
- /// 仅「允许首次创建的 TransType(如 rct-po-ins 入库)」在守卫通过后才 bootstrap。选项 A(正确,不污染存在性语义)。
- /// </summary>
- 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);
- }
- }
|