InventoryGuardValidatorTests.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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 9 道前置守卫正反测试(Phase 5A;纯校验,无 DB)。
  6. /// LIVE 实核为 9 道;旧方案文档「8 道」为不完整总结(遗漏 G8 iss-tr-ins 质检过账≤质检库存)。
  7. /// 实施以 pr_SFM_InventoryTransactionProcessing2 为最终契约。
  8. /// </summary>
  9. public class InventoryGuardValidatorTests
  10. {
  11. private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1", string lot = "", string refs = "")
  12. => new InventoryTransactionCommand { TransType = tt, ChangeQty = qty, ItemNum = item, Location = loc, LotSerial = lot, Refs = refs };
  13. private static InventoryContext Ctx(
  14. bool lotControl = false, bool dup = false, decimal avail = 1000m, decimal freeze = 1000m, decimal assay = 1000m,
  15. bool withItem = true, bool withLoc = true, bool withInvMaster = true, bool withDetail = true,
  16. string item = "IT-1", string loc = "L1", string lot = "", string refs = "")
  17. {
  18. var ctx = new InventoryContext { IsLotControl = lotControl, HasDuplicateBaseData = dup };
  19. if (withItem) ctx.Items.Add(item);
  20. if (withLoc) ctx.Locations.Add(loc);
  21. if (withInvMaster) ctx.InvMasters[InventoryContext.InvKey(item, loc)] = new InventoryBalance { QtyOnHand = 1000m, AvailStatusQty = avail, Assay = assay, FreezeQty = freeze };
  22. if (withDetail)
  23. {
  24. var k = lotControl ? InventoryContext.DetailKey(item, loc, lot, refs) : InventoryContext.DetailKey(item, loc, "", "");
  25. ctx.LocationDetails[k] = new InventoryBalance { QtyOnHand = 1000m, AvailStatusQty = avail, Assay = assay, FreezeQty = freeze };
  26. }
  27. return ctx;
  28. }
  29. private static List<InventoryTransactionCommand> L(params InventoryTransactionCommand[] c) => c.ToList();
  30. [Fact]
  31. public void G1_DuplicateBaseData_Fails()
  32. {
  33. var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(dup: true));
  34. Assert.Single(e);
  35. Assert.Equal(InventoryGuardCode.DuplicateBaseData, e[0].Code);
  36. }
  37. [Fact]
  38. public void G2_ItemMasterMissing_Fails_Present_Passes()
  39. {
  40. var bad = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withItem: false));
  41. Assert.Contains(bad, x => x.Code == InventoryGuardCode.ItemMasterMissing);
  42. var ok = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx());
  43. Assert.Empty(ok);
  44. }
  45. [Fact]
  46. public void G3_LocationMasterMissing_Fails()
  47. {
  48. var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withLoc: false));
  49. Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationMasterMissing);
  50. }
  51. [Fact]
  52. public void G4_InvMasterMissing_OnNetOutbound_Fails()
  53. {
  54. // 净出库 且 无 InvMaster
  55. var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10)), Ctx(withInvMaster: false, withDetail: false));
  56. Assert.Contains(e, x => x.Code == InventoryGuardCode.InvMasterMissing);
  57. // 入库则不触发 G4
  58. var ok = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withInvMaster: false, withDetail: false));
  59. Assert.DoesNotContain(ok, x => x.Code == InventoryGuardCode.InvMasterMissing);
  60. }
  61. [Fact]
  62. public void G5_LocationDetailMissing_OnNetOutbound_Fails()
  63. {
  64. var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10)), Ctx(withDetail: false));
  65. Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationDetailMissing);
  66. }
  67. [Fact]
  68. public void G6_UnfreezeExceedsFreeze_Fails_Sufficient_Passes()
  69. {
  70. var bad = InventoryGuardValidator.Validate(L(Cmd("inv-unfrz", -100)), Ctx(freeze: 50m));
  71. Assert.Contains(bad, x => x.Code == InventoryGuardCode.UnfreezeExceedsFreeze);
  72. var ok = InventoryGuardValidator.Validate(L(Cmd("inv-unfrz", -100)), Ctx(freeze: 200m));
  73. Assert.Empty(ok);
  74. }
  75. [Fact]
  76. public void G7_FreezeExceedsAvail_Fails()
  77. {
  78. var e = InventoryGuardValidator.Validate(L(Cmd("inv-frz", -100)), Ctx(avail: 50m));
  79. Assert.Contains(e, x => x.Code == InventoryGuardCode.FreezeExceedsAvail);
  80. }
  81. [Fact]
  82. public void G8_AssayIssueExceedsAssay_Fails()
  83. {
  84. // 旧总结遗漏的一道
  85. var e = InventoryGuardValidator.Validate(L(Cmd("iss-tr-ins", -100)), Ctx(assay: 50m));
  86. Assert.Contains(e, x => x.Code == InventoryGuardCode.AssayIssueExceedsAssay);
  87. }
  88. [Fact]
  89. public void G9_IssueExceedsAvail_Fails()
  90. {
  91. var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -100)), Ctx(avail: 50m));
  92. Assert.Contains(e, x => x.Code == InventoryGuardCode.IssueExceedsAvail);
  93. }
  94. [Fact]
  95. public void AllGuardsPass_RctPoInsInbound_NoErrors()
  96. {
  97. var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 100)), Ctx());
  98. Assert.Empty(e);
  99. }
  100. [Fact]
  101. public void LotControl_G5_MissingBatch_IncludesLot()
  102. {
  103. var e = InventoryGuardValidator.Validate(
  104. L(Cmd("iss-x", -10, lot: "LOT-A")),
  105. Ctx(lotControl: true, withDetail: false, lot: "LOT-A"));
  106. var err = Assert.Single(e, x => x.Code == InventoryGuardCode.LocationDetailMissing);
  107. Assert.Equal("LOT-A", err.LotSerial);
  108. }
  109. [Fact]
  110. public void LotControl_DifferentLot_IsolatedStock()
  111. {
  112. // LOT-A 有库存、LOT-B 无 → 出 LOT-B 触发 G5,不受 LOT-A 影响
  113. var ctx = Ctx(lotControl: true, withDetail: true, lot: "LOT-A");
  114. var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10, lot: "LOT-B")), ctx);
  115. Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationDetailMissing && x.LotSerial == "LOT-B");
  116. }
  117. }