IqcReceiptOrchestratorTests.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
  3. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
  4. using Xunit;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  6. /// <summary>
  7. /// C2 IqcReceiptOrchestrator 事务组合测试(Phase 6B-2)。外层单事务、C6/C5 同成同败;
  8. /// 事务组合为语义单测,非 MySQL 实证(Phase 5B)。
  9. /// </summary>
  10. public class IqcReceiptOrchestratorTests
  11. {
  12. private const long Tenant = 797403760988229L;
  13. private static IqcAcceptedReceipt Receipt(decimal accepted = 100m, decimal sample = 0m, string item = "IT-1", string loc = "L1")
  14. => new IqcAcceptedReceipt
  15. {
  16. TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, RctNbr = "RCT-1",
  17. ItemNum = item, Location = loc, LotSerial = "", AcceptedQty = accepted, SampleQty = sample,
  18. UmConversion = 1m, PurOrd = "PO-1", PurLine = 1, User = "t", RctQcNbr = "QC-1", Receiver = "RCV-1", Fbillno = "FB-1",
  19. };
  20. private static PurchaseReceiptGateInput GateOk()
  21. => new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = true };
  22. private static FakeInventoryDatabase DbWithMaster()
  23. {
  24. var db = new FakeInventoryDatabase();
  25. db.SeedMaster(Tenant, "8010", "IT-1", "L1");
  26. return db;
  27. }
  28. private static IqcReceiptOrchestrator Orch(FakeInventoryDatabase db, FakeReceiptFailPhase fail = FakeReceiptFailPhase.None)
  29. => new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db, fail));
  30. // ── T4 全成 ──
  31. [Fact]
  32. public async Task T4_AllSuccess_CommitsAll_C6ThenC5()
  33. {
  34. var db = DbWithMaster();
  35. var r = await Orch(db).PostAsync(Receipt(), GateOk());
  36. Assert.Equal(IqcReceiptOutcome.Success, r.Outcome);
  37. Assert.Equal(1, db.InvMaster.Count);
  38. Assert.Equal(1, db.LocationDetail.Count);
  39. Assert.Equal(1, db.Transactions.Count);
  40. Assert.Equal(1, db.ReceiptWriteback.Count); // C6 shadow
  41. Assert.Equal(1, db.MobileTasks.Count); // C7 InvUpShelf
  42. Assert.Equal(new[] { "C6", "C5", "C7", "COMPLETE" }, db.CallOrder.ToArray()); // 顺序 C6→C5→C7→完成锚
  43. Assert.Equal(0, db.BarcodeCardWrites); // C3 不可达
  44. }
  45. // ── T1 C6 成功 / C5 失败 → 全回滚 ──
  46. [Fact]
  47. public async Task T1_C6Ok_C5Fail_AllRollback()
  48. {
  49. var db = DbWithMaster();
  50. var r = await Orch(db, FakeReceiptFailPhase.C5).PostAsync(Receipt(), GateOk());
  51. Assert.Equal(IqcReceiptOutcome.Error, r.Outcome);
  52. Assert.Equal(0, db.TotalPersisted); // C6 也回滚
  53. }
  54. // ── T2 C5 成功 / 下游(post-C5)失败 → 全回滚 ──
  55. [Fact]
  56. public async Task T2_C5Ok_DownstreamFail_AllRollback()
  57. {
  58. var db = DbWithMaster();
  59. var r = await Orch(db).PostAsync(Receipt(), GateOk(),
  60. postC5Hook: () => throw new InvalidOperationException("downstream(C7/MissedPrint) fail"));
  61. Assert.Equal(IqcReceiptOutcome.Error, r.Outcome);
  62. Assert.Equal(0, db.TotalPersisted); // C6 + C5 + C7 全滚
  63. Assert.Equal(new[] { "C6", "C5", "C7" }, db.CallOrder.ToArray()); // C7 后 post-hook 抛错
  64. }
  65. // ── T3 C6 失败 → C5 不调用、0 落地 ──
  66. [Fact]
  67. public async Task T3_C6Fail_C5NotCalled_ZeroWrites()
  68. {
  69. var db = DbWithMaster();
  70. var r = await Orch(db, FakeReceiptFailPhase.C6).PostAsync(Receipt(), GateOk());
  71. Assert.Equal(IqcReceiptOutcome.Error, r.Outcome);
  72. Assert.Equal(0, db.TotalPersisted);
  73. Assert.Equal(new[] { "C6" }, db.CallOrder.ToArray()); // C5 未调用
  74. }
  75. // ── Gate 未满足 → Skipped, 0 写 ──
  76. [Fact]
  77. public async Task Gate_NotSatisfied_Skipped_ZeroWrites()
  78. {
  79. var db = DbWithMaster();
  80. var gate = new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 0m, HasReceivableDetails = true };
  81. var r = await Orch(db).PostAsync(Receipt(), gate);
  82. Assert.Equal(IqcReceiptOutcome.Skipped, r.Outcome);
  83. Assert.Equal(0, db.TotalPersisted);
  84. Assert.Empty(db.CallOrder); // 未进事务
  85. }
  86. // ── 9 守卫失败 → ValidationFailure, 0 写 ──
  87. [Fact]
  88. public async Task Guard_Failure_ValidationFailure_ZeroWrites()
  89. {
  90. var db = new FakeInventoryDatabase(); // 无 master → G2
  91. var r = await Orch(db).PostAsync(Receipt(), GateOk());
  92. Assert.Equal(IqcReceiptOutcome.ValidationFailure, r.Outcome);
  93. Assert.Equal(0, db.TotalPersisted);
  94. }
  95. // ── 留样:C5 后最终 +Accepted-Sample ──
  96. [Fact]
  97. public async Task WithSample_FinalBalance_AcceptedMinusSample()
  98. {
  99. var db = DbWithMaster();
  100. var r = await Orch(db).PostAsync(Receipt(accepted: 100m, sample: 10m), GateOk());
  101. Assert.Equal(IqcReceiptOutcome.Success, r.Outcome);
  102. Assert.Equal(90m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].QtyOnHand);
  103. Assert.Equal(90m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].AvailStatusQty);
  104. Assert.Equal(2, db.Transactions.Count); // rct-po + iss-unp
  105. }
  106. // ── MRB → 是否进 C5(C1↔C2 连接)──
  107. [Theory]
  108. [InlineData(0, "", true)] // ① 合格无处理 → 进
  109. [InlineData(0, "3", false)] // ② 挑选 → 不进
  110. [InlineData(1, "0", false)] // ③ 退货 → 不进
  111. [InlineData(1, "2", true)] // ④ 让步接收 → 进
  112. [InlineData(1, "3", false)] // ⑤ 挑选 → 不进
  113. public void Mrb_EntersInventoryPosting_MatchesC5Routing(int pd, string clfs, bool entersC5)
  114. {
  115. var m = IqcResultMapping.Map(new IqcResultMappingInput { Pd = pd, Clfs = clfs, Dhsl = 10m, Bhgsl = 3m });
  116. Assert.Equal(entersC5, m.EntersInventoryPosting);
  117. }
  118. }