| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- 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;
- /// <summary>
- /// C2 IqcReceiptOrchestrator 事务组合测试(Phase 6B-2)。外层单事务、C6/C5 同成同败;
- /// 事务组合为语义单测,非 MySQL 实证(Phase 5B)。
- /// </summary>
- 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);
- }
- }
|