using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// C2 IqcReceiptOrchestrator 事务组合测试(Phase 6B-2)。外层单事务、C6/C5 同成同败; /// 事务组合为语义单测,非 MySQL 实证(Phase 5B)。 /// public class IqcReceiptOrchestratorTests { private const long Tenant = 797403760988229L; private static IqcAcceptedReceipt Receipt(decimal accepted = 100m, decimal sample = 0m, string item = "IT-1", string loc = "L1") => new IqcAcceptedReceipt { TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, RctNbr = "RCT-1", ItemNum = item, Location = loc, LotSerial = "", AcceptedQty = accepted, SampleQty = sample, UmConversion = 1m, PurOrd = "PO-1", PurLine = 1, User = "t", RctQcNbr = "QC-1", Receiver = "RCV-1", Fbillno = "FB-1", }; private static PurchaseReceiptGateInput GateOk() => new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = true }; private static FakeInventoryDatabase DbWithMaster() { var db = new FakeInventoryDatabase(); db.SeedMaster(Tenant, "8010", "IT-1", "L1"); return db; } private static IqcReceiptOrchestrator Orch(FakeInventoryDatabase db, FakeReceiptFailPhase fail = FakeReceiptFailPhase.None) => new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db, fail)); // ── T4 全成 ── [Fact] public async Task T4_AllSuccess_CommitsAll_C6ThenC5() { var db = DbWithMaster(); var r = await Orch(db).PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Success, r.Outcome); Assert.Equal(1, db.InvMaster.Count); Assert.Equal(1, db.LocationDetail.Count); Assert.Equal(1, db.Transactions.Count); Assert.Equal(1, db.ReceiptWriteback.Count); // C6 shadow Assert.Equal(1, db.MobileTasks.Count); // C7 InvUpShelf Assert.Equal(new[] { "C6", "C5", "C7", "COMPLETE" }, db.CallOrder.ToArray()); // 顺序 C6→C5→C7→完成锚 Assert.Equal(0, db.BarcodeCardWrites); // C3 不可达 } // ── T1 C6 成功 / C5 失败 → 全回滚 ── [Fact] public async Task T1_C6Ok_C5Fail_AllRollback() { var db = DbWithMaster(); var r = await Orch(db, FakeReceiptFailPhase.C5).PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Error, r.Outcome); Assert.Equal(0, db.TotalPersisted); // C6 也回滚 } // ── T2 C5 成功 / 下游(post-C5)失败 → 全回滚 ── [Fact] public async Task T2_C5Ok_DownstreamFail_AllRollback() { var db = DbWithMaster(); var r = await Orch(db).PostAsync(Receipt(), GateOk(), postC5Hook: () => throw new InvalidOperationException("downstream(C7/MissedPrint) fail")); Assert.Equal(IqcReceiptOutcome.Error, r.Outcome); Assert.Equal(0, db.TotalPersisted); // C6 + C5 + C7 全滚 Assert.Equal(new[] { "C6", "C5", "C7" }, db.CallOrder.ToArray()); // C7 后 post-hook 抛错 } // ── T3 C6 失败 → C5 不调用、0 落地 ── [Fact] public async Task T3_C6Fail_C5NotCalled_ZeroWrites() { var db = DbWithMaster(); var r = await Orch(db, FakeReceiptFailPhase.C6).PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Error, r.Outcome); Assert.Equal(0, db.TotalPersisted); Assert.Equal(new[] { "C6" }, db.CallOrder.ToArray()); // C5 未调用 } // ── Gate 未满足 → Skipped, 0 写 ── [Fact] public async Task Gate_NotSatisfied_Skipped_ZeroWrites() { var db = DbWithMaster(); var gate = new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 0m, HasReceivableDetails = true }; var r = await Orch(db).PostAsync(Receipt(), gate); Assert.Equal(IqcReceiptOutcome.Skipped, r.Outcome); Assert.Equal(0, db.TotalPersisted); Assert.Empty(db.CallOrder); // 未进事务 } // ── 9 守卫失败 → ValidationFailure, 0 写 ── [Fact] public async Task Guard_Failure_ValidationFailure_ZeroWrites() { var db = new FakeInventoryDatabase(); // 无 master → G2 var r = await Orch(db).PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.ValidationFailure, r.Outcome); Assert.Equal(0, db.TotalPersisted); } // ── 留样:C5 后最终 +Accepted-Sample ── [Fact] public async Task WithSample_FinalBalance_AcceptedMinusSample() { var db = DbWithMaster(); var r = await Orch(db).PostAsync(Receipt(accepted: 100m, sample: 10m), GateOk()); Assert.Equal(IqcReceiptOutcome.Success, r.Outcome); Assert.Equal(90m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].QtyOnHand); Assert.Equal(90m, db.InvMaster[FakeInventoryDatabase.InvK(Tenant, "8010", "IT-1", "L1")].AvailStatusQty); Assert.Equal(2, db.Transactions.Count); // rct-po + iss-unp } // ── MRB → 是否进 C5(C1↔C2 连接)── [Theory] [InlineData(0, "", true)] // ① 合格无处理 → 进 [InlineData(0, "3", false)] // ② 挑选 → 不进 [InlineData(1, "0", false)] // ③ 退货 → 不进 [InlineData(1, "2", true)] // ④ 让步接收 → 进 [InlineData(1, "3", false)] // ⑤ 挑选 → 不进 public void Mrb_EntersInventoryPosting_MatchesC5Routing(int pd, string clfs, bool entersC5) { var m = IqcResultMapping.Map(new IqcResultMappingInput { Pd = pd, Clfs = clfs, Dhsl = 10m, Bhgsl = 3m }); Assert.Equal(entersC5, m.EntersInventoryPosting); } }