InventoryTransactionServiceTests.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>
  5. /// C5 InventoryTransactionService 编排测试(Phase 5A/5C):写集路由 + 守卫 0 写 + 故障注入全回滚(事务语义单测)。
  6. /// 单事务/锁边界经 UnitOfWork;故障注入为事务语义单测,非 MySQL rollback 实证(Phase 5B)。
  7. /// </summary>
  8. public class InventoryTransactionServiceTests
  9. {
  10. private const long Tenant = 797403760988229L;
  11. private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1", string lot = "", string refs = "")
  12. => new InventoryTransactionCommand
  13. {
  14. TenantId = Tenant, DomainCode = "8010",
  15. PostingId = 1, TransactionGroupId = 100, Seq = 1,
  16. TransType = tt, ChangeQty = qty, UmConversion = 1m,
  17. ItemNum = item, Location = loc, LotSerial = lot, Refs = refs, User = "t",
  18. };
  19. // ── InventoryContext(供 BuildWriteSet 纯路由测试)──
  20. private static InventoryContext EmptyCtxWithMaster(string item = "IT-1", string loc = "L1")
  21. {
  22. var ctx = new InventoryContext();
  23. ctx.Items.Add(item);
  24. ctx.Locations.Add(loc);
  25. return ctx;
  26. }
  27. private static InventoryContext StockedCtx(string item = "IT-1", string loc = "L1", decimal stock = 500m)
  28. {
  29. var ctx = new InventoryContext();
  30. ctx.Items.Add(item);
  31. ctx.Locations.Add(loc);
  32. ctx.InvMasters[InventoryContext.InvKey(item, loc)] = new InventoryBalance { QtyOnHand = stock, AvailStatusQty = stock, Assay = stock, FreezeQty = stock };
  33. ctx.LocationDetails[InventoryContext.DetailKey(item, loc, "", "")] = new InventoryBalance { QtyOnHand = stock, AvailStatusQty = stock, Assay = stock, FreezeQty = stock };
  34. ctx.ItemDefaultLocation[item] = loc;
  35. return ctx;
  36. }
  37. // ── FakeInventoryDatabase(供 PostAsync 编排测试)──
  38. private static FakeInventoryDatabase DbWithMaster()
  39. {
  40. var db = new FakeInventoryDatabase();
  41. db.SeedMaster(Tenant, "8010", "IT-1", "L1");
  42. return db;
  43. }
  44. private static InventoryTransactionService Svc(FakeInventoryDatabase db, FakeInventoryFailPhase fail = FakeInventoryFailPhase.None)
  45. => new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db, fail));
  46. // ═══ 路由:INSERT vs UPDATE(BuildWriteSet 纯计算)═══
  47. [Fact]
  48. public void BuildWriteSet_InboundToEmpty_RoutesInsert()
  49. {
  50. var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 100m) }, EmptyCtxWithMaster());
  51. Assert.True(Assert.Single(ws.InvMasterWrites).InsertIfMissing);
  52. Assert.True(Assert.Single(ws.LocationDetailWrites).InsertIfMissing);
  53. Assert.Single(ws.ItemLocationWrites);
  54. Assert.Single(ws.TransactionRows);
  55. }
  56. [Fact]
  57. public void BuildWriteSet_OutboundToExisting_RoutesUpdateOnly()
  58. {
  59. var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("iss-x", -50m) }, StockedCtx());
  60. Assert.False(Assert.Single(ws.InvMasterWrites).InsertIfMissing);
  61. Assert.False(Assert.Single(ws.LocationDetailWrites).InsertIfMissing);
  62. Assert.Empty(ws.ItemLocationWrites);
  63. }
  64. [Fact]
  65. public void BuildWriteSet_RctPoIns_AfterBalances_MatchLive()
  66. {
  67. var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 40m) }, StockedCtx(stock: 100m));
  68. var inv = Assert.Single(ws.InvMasterWrites);
  69. Assert.Equal(140m, inv.After.QtyOnHand);
  70. Assert.Equal(100m, inv.After.AvailStatusQty);
  71. Assert.Equal(140m, inv.After.Assay);
  72. }
  73. [Fact]
  74. public void BuildWriteSet_NonLot_LotSerialEmpty()
  75. => Assert.Equal("", Assert.Single(
  76. InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 10m) }, EmptyCtxWithMaster()).LocationDetailWrites).LotSerial);
  77. [Fact]
  78. public void BuildWriteSet_Lot_PreservesLotSerial()
  79. => Assert.Equal("LOT-A", Assert.Single(
  80. InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 10m, lot: "LOT-A") }, EmptyCtxWithMaster()).LocationDetailWrites).LotSerial);
  81. // ═══ PostAsync 编排 ═══
  82. [Fact]
  83. public async Task Post_GuardFailure_ZeroWrites()
  84. {
  85. var db = new FakeInventoryDatabase(); // 无 master → G2
  86. var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 10m) });
  87. Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
  88. Assert.Equal(0, r.WrittenRows);
  89. Assert.Equal(0, db.TotalPersisted);
  90. Assert.NotEmpty(r.GuardErrors);
  91. }
  92. [Fact]
  93. public async Task Post_Success_PersistsAllFour()
  94. {
  95. var db = DbWithMaster();
  96. var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 100m) });
  97. Assert.Equal(InventoryPostingOutcome.Success, r.Outcome);
  98. Assert.Equal("保存成功", r.LegacyReturnMsg);
  99. Assert.Equal(1, db.InvMaster.Count);
  100. Assert.Equal(1, db.LocationDetail.Count);
  101. Assert.Equal(1, db.ItemLocation.Count);
  102. Assert.Equal(1, db.Transactions.Count);
  103. }
  104. [Theory]
  105. [InlineData(FakeInventoryFailPhase.InvMaster)]
  106. [InlineData(FakeInventoryFailPhase.ItemLocation)]
  107. [InlineData(FakeInventoryFailPhase.LocationDetail)]
  108. [InlineData(FakeInventoryFailPhase.Transaction)]
  109. public async Task Post_FailureInjection_AllRollBack(FakeInventoryFailPhase phase)
  110. {
  111. var db = DbWithMaster();
  112. var r = await Svc(db, phase).PostAsync(new[] { Cmd("rct-po-ins", 100m) });
  113. Assert.Equal(InventoryPostingOutcome.Error, r.Outcome);
  114. Assert.Equal(0, r.WrittenRows);
  115. Assert.Equal(0, db.TotalPersisted);
  116. Assert.StartsWith("保存失败,", r.LegacyReturnMsg);
  117. }
  118. [Fact]
  119. public async Task Post_EmptyCommands_SuccessZero()
  120. {
  121. var r = await Svc(new FakeInventoryDatabase()).PostAsync(System.Array.Empty<InventoryTransactionCommand>());
  122. Assert.Equal(InventoryPostingOutcome.Success, r.Outcome);
  123. Assert.Equal(0, r.WrittenRows);
  124. }
  125. }