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; /// /// Phase 7-1:C7 移动任务(InvUpShelf / PurOrdRctByMRBChoose)+ MRB 五分支最终路由 + C3 排除。 /// 据 Phase 6A/7 LIVE:①④ 收货→C5+InvUpShelf;②⑤ 挑选→PurOrdRctByMRBChoose 任务(无 C5);③ 退货→无任务无 C5。 /// public class IqcC7AndMrbRoutingTests { private const long T1 = 797403760988229L; private const long T2 = 824585161322565L; private static IqcAcceptedReceipt Receipt(long tenant = T1, string receiver = "RCV-1") => new IqcAcceptedReceipt { TenantId = tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, RctNbr = "RCT-1", ItemNum = "IT-1", Location = "L1", AcceptedQty = 100m, UmConversion = 1m, PurOrd = "PO-1", PurLine = 1, Potype = "po", OrderedQty = 100m, BeforeReceivedQty = 0m, Receiver = receiver, RctQcNbr = "QC-1", Fbillno = "FB-1", User = "t", }; private static PurchaseReceiptGateInput GateOk() => new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = true }; private static IqcMrbSelectionCommand MrbCmd(long tenant = T1, string qcNbr = "QC-9") => new IqcMrbSelectionCommand { TenantId = tenant, DomainCode = "8010", PostingId = 2, QcNbr = qcNbr, ItemNum = "IT-1", Fbillno = "FB-2", Receiver = "RCV-2", User = "t" }; private static FakeInventoryDatabase DbWithMaster() { var db = new FakeInventoryDatabase(); db.SeedMaster(T1, "8010", "IT-1", "L1"); db.SeedMaster(T2, "8010", "IT-1", "L1"); return db; } // ── C7 InvUpShelf 经编排创建 ── [Fact] public async Task C7_InvUpShelf_CreatedViaOrchestrator() { var db = DbWithMaster(); var orch = new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db)); var r = await orch.PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Success, r.Outcome); var task = db.MobileTasks[$"{T1}|8010|RCV-1"]; Assert.Equal("InvUpShelf", task.Name); Assert.Equal("RCV-1", task.TaskId); // TaskID=Receiver } // ── T7:C7 失败 → C6/C5/C7 全回滚 ── [Fact] public async Task T7_C7Fail_AllRollback() { var db = DbWithMaster(); var orch = new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db, FakeReceiptFailPhase.C7)); var r = await orch.PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Error, r.Outcome); Assert.Equal(0, db.TotalPersisted); // C6+C5+C7 全滚 Assert.Equal(new[] { "C6", "C5", "C7" }, db.CallOrder.ToArray()); // C7 抛错→完成锚未达 } // ── TaskID 重复 upsert(同 QCNbr 两次 MRB 选择 → 一个任务)── [Fact] public async Task T9_SameTaskId_UpsertsToOne() { var db = DbWithMaster(); var svc = new IqcMrbSelectionTaskService(new FakeIqcReceiptUnitOfWorkFactory(db)); await svc.PostAsync(MrbCmd(qcNbr: "QC-DUP")); await svc.PostAsync(MrbCmd(qcNbr: "QC-DUP")); Assert.Equal(1, db.MobileTasks.Count); // 幂等 upsert → 1 } // ── T10:同 TaskID 不同 tenant → 各自独立任务 ── [Fact] public async Task T10_SameTaskId_DifferentTenant_Isolated() { var db = DbWithMaster(); var svc = new IqcMrbSelectionTaskService(new FakeIqcReceiptUnitOfWorkFactory(db)); await svc.PostAsync(MrbCmd(tenant: T1, qcNbr: "QC-X")); await svc.PostAsync(MrbCmd(tenant: T2, qcNbr: "QC-X")); Assert.Equal(2, db.MobileTasks.Count); // tenant 隔离 } // ── MRB ①④:C5 + InvUpShelf(经编排)── [Theory] [InlineData(0, "")] // ① 合格无处理 [InlineData(1, "2")] // ④ 让步接收 public async Task Mrb1And4_Orchestrator_C5PlusInvUpShelf(int pd, string clfs) { var m = IqcResultMapping.Map(new IqcResultMappingInput { Pd = pd, Clfs = clfs, Dhsl = 100m, Bhgsl = 0m }); Assert.True(m.EntersInventoryPosting); // 路由=进 C5 var db = DbWithMaster(); var r = await new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db)).PostAsync(Receipt(), GateOk()); Assert.Equal(IqcReceiptOutcome.Success, r.Outcome); Assert.True(db.Transactions.Count > 0); // C5 过账 Assert.Equal(1, db.MobileTasks.Count); // InvUpShelf Assert.Equal("InvUpShelf", db.MobileTasks[$"{T1}|8010|RCV-1"].Name); } // ── MRB ②⑤:PurOrdRctByMRBChoose 任务,无 C5 ── [Theory] [InlineData(0, "3")] // ② 挑选 [InlineData(1, "3")] // ⑤ 挑选 public async Task Mrb2And5_MrbSelection_ChooseTask_NoC5(int pd, string clfs) { var m = IqcResultMapping.Map(new IqcResultMappingInput { Pd = pd, Clfs = clfs, Dhsl = 100m, Bhgsl = 3m }); Assert.False(m.EntersInventoryPosting); // 路由=不进 C5 Assert.Equal(IqcRoutingKind.MrbChooseMobileTask, m.RoutingKind); var db = DbWithMaster(); var r = await new IqcMrbSelectionTaskService(new FakeIqcReceiptUnitOfWorkFactory(db)).PostAsync(MrbCmd()); Assert.Equal(IqcReceiptOutcome.Success, r.Outcome); Assert.Equal(1, db.MobileTasks.Count); Assert.Equal("PurOrdRctByMRBChoose", db.MobileTasks[$"{T1}|8010|QC-9"].Name); Assert.Equal(0, db.Transactions.Count); // 无 C5 过账 Assert.Equal(0, db.InvMaster.Count); } // ── MRB ③ 退货:无 C5、无任务 ── [Fact] public void Mrb3_ReturnOnly_NoC5_NoTask() { var m = IqcResultMapping.Map(new IqcResultMappingInput { Pd = 1, Clfs = "0", Dhsl = 10m, Bhgsl = 3m }); Assert.False(m.EntersInventoryPosting); Assert.Equal(IqcRoutingKind.ReturnOnly, m.RoutingKind); // 既不进 C5 也不建挑选任务 } // ── C3 仍为 0(接线后不复现)── [Fact] public async Task C3_StaysZero_AfterC7Wiring() { var db = DbWithMaster(); await new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db)).PostAsync(Receipt(), GateOk()); Assert.Equal(0, db.BarcodeCardWrites); } }