InventoryBootstrapGuardTests.cs 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>
  5. /// Phase 5C 最后复核:证明 UoW 的「INSERT…ON DUPLICATE KEY(noop) 首建 bootstrap」**不绕过 G4/G5**。
  6. /// 守卫在 LockAndLoad(只读既有行) 之后、Write(含 bootstrap) 之前运行 → 缺行时 G4/G5 先命中、0 写;
  7. /// 仅「允许首次创建的 TransType(如 rct-po-ins 入库)」在守卫通过后才 bootstrap。选项 A(正确,不污染存在性语义)。
  8. /// </summary>
  9. public class InventoryBootstrapGuardTests
  10. {
  11. private const long Tenant = 797403760988229L;
  12. private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1")
  13. => new InventoryTransactionCommand
  14. {
  15. TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, Seq = 1,
  16. TransType = tt, ChangeQty = qty, UmConversion = 1m, ItemNum = item, Location = loc, User = "t",
  17. };
  18. private static InventoryTransactionService Svc(FakeInventoryDatabase db)
  19. => new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db));
  20. // Test 1:净出库 + InvMaster 不存在 → 首命中 G4(InvMasterMissing),非 G9;且不 bootstrap
  21. [Fact]
  22. public async Task NetOutbound_NoInvMaster_HitsG4_NotG9_NoBootstrap()
  23. {
  24. var db = new FakeInventoryDatabase();
  25. db.SeedMaster(Tenant, "8010", "IT-1", "L1"); // 主数据在,无库存行
  26. var r = await Svc(db).PostAsync(new[] { Cmd("iss-x", -10m) });
  27. Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
  28. Assert.Equal(InventoryGuardCode.InvMasterMissing, r.GuardErrors[0].Code); // 首命中 G4
  29. Assert.Equal(0, db.InvMaster.Count); // 未 bootstrap 占位行
  30. Assert.Equal(0, db.LocationDetail.Count);
  31. Assert.Equal(0, db.TotalPersisted);
  32. }
  33. // Test 2:净出库 + InvMaster 存在但 LocationDetail 不存在 → 首命中 G5(LocationDetailMissing);且不 bootstrap 明细
  34. [Fact]
  35. public async Task NetOutbound_InvMasterOnly_NoLocationDetail_HitsG5_NoBootstrap()
  36. {
  37. var db = new FakeInventoryDatabase();
  38. db.SeedMaster(Tenant, "8010", "IT-1", "L1");
  39. db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")] =
  40. new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 100m, Assay = 100m, FreezeQty = 100m };
  41. var r = await Svc(db).PostAsync(new[] { Cmd("iss-x", -10m) });
  42. Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
  43. Assert.Equal(InventoryGuardCode.LocationDetailMissing, r.GuardErrors[0].Code); // 首命中 G5
  44. Assert.Equal(0, db.LocationDetail.Count); // 未 bootstrap 明细行
  45. Assert.Equal(1, db.InvMaster.Count); // 既有 InvMaster 未被改动/新增
  46. }
  47. // Test 3:rct-po-ins(允许首次创建)+ 两表均不存在 → 允许 bootstrap 并成功
  48. [Fact]
  49. public async Task RctPoIns_BothMissing_BootstrapAndSucceed()
  50. {
  51. var db = new FakeInventoryDatabase();
  52. db.SeedMaster(Tenant, "8010", "IT-1", "L1");
  53. var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 100m) });
  54. Assert.Equal(InventoryPostingOutcome.Success, r.Outcome);
  55. Assert.Equal(1, db.InvMaster.Count); // 首建
  56. Assert.Equal(1, db.LocationDetail.Count);
  57. Assert.Equal(100m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].QtyOnHand);
  58. }
  59. }