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 6B 纯逻辑件测试:Runtime Gate + CheckInventoryStatus 轻校验 + IqcInventoryBatchBuilder。 /// 据 Phase 6A LIVE 契约:IQC 收货 `rct-po`(→Avail,Assay=0),留样 `iss-unp`(-Sample);最终 +Accepted-Sample。 /// public class IqcReceiptPureTests { // ── Runtime Gate ── [Fact] public void Gate_AllSatisfied_CanCreate() => Assert.True(PurchaseReceiptRuntimeGate.Evaluate( new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = true }).CanCreate); [Fact] public void Gate_AcceptedQtyZero_CannotCreate() => Assert.False(PurchaseReceiptRuntimeGate.Evaluate( new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 0m, HasReceivableDetails = true }).CanCreate); [Fact] public void Gate_NoReceivableDetails_CannotCreate() => Assert.False(PurchaseReceiptRuntimeGate.Evaluate( new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = false }).CanCreate); [Fact] public void Gate_NoPendingReceipt_CannotCreate() => Assert.False(PurchaseReceiptRuntimeGate.Evaluate( new PurchaseReceiptGateInput { HasPendingReceipt = false, AcceptedQty = 100m, HasReceivableDetails = true }).CanCreate); // ── CheckInventoryStatus 轻校验 ── [Fact] public void LocationCheck_AllNonEmpty_Ok() { var cmds = new[] { new InventoryTransactionCommand { ItemNum = "IT-1", Location = "L1" } }; Assert.True(InventoryLocationCheck.Validate(cmds).Ok); } [Fact] public void LocationCheck_EmptyLocation_Fails() { var cmds = new[] { new InventoryTransactionCommand { ItemNum = "IT-1", Location = "" } }; var r = InventoryLocationCheck.Validate(cmds); Assert.False(r.Ok); Assert.Contains("IT-1", r.OffendingItems); } // ── Batch Builder ── private static IqcAcceptedReceipt Receipt(decimal accepted, decimal sample) => new IqcAcceptedReceipt { TenantId = 797403760988229L, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, RctNbr = "RCT-001", ItemNum = "IT-1", Location = "L1", LotSerial = "", AcceptedQty = accepted, SampleQty = sample, UmConversion = 1m, User = "t", }; [Fact] public void Build_NoSample_OnlyRctPo() { var b = IqcInventoryBatchBuilder.Build(Receipt(100m, 0m)); var c = Assert.Single(b.Commands); Assert.Equal(InventoryTransType.RctPo, c.TransType); Assert.Equal(100m, c.ChangeQty); Assert.Equal("RCT-001", c.OrdNbr); Assert.Equal(1, c.Seq); } [Fact] public void Build_WithSample_RctPoThenIssUnp_OrderAndSigns() { var b = IqcInventoryBatchBuilder.Build(Receipt(100m, 10m)); Assert.Equal(2, b.Commands.Count); Assert.Equal(InventoryTransType.RctPo, b.Commands[0].TransType); Assert.Equal(100m, b.Commands[0].ChangeQty); Assert.Equal(InventoryTransType.IssUnp, b.Commands[1].TransType); Assert.Equal(-10m, b.Commands[1].ChangeQty); // 留样恒负 Assert.Equal("iqc", b.Commands[1].Reason); Assert.Equal(1, b.Commands[0].Seq); Assert.Equal(2, b.Commands[1].Seq); } // 经 Calculator 验证最终库存语义 [Fact] public void Build_RctPoAlone_IntoAvail_NotAssay() { var b = IqcInventoryBatchBuilder.Build(Receipt(100m, 0m)); var after = InventoryBalanceCalculator.Project(new InventoryBalance(), b.Commands[0]).After; Assert.Equal(100m, after.QtyOnHand); Assert.Equal(100m, after.AvailStatusQty); // 进可用库存 Assert.Equal(0m, after.Assay); // 非质检(修正 rct-po-ins 旧误判) } [Fact] public void Build_WithSample_FinalBalance_AcceptedMinusSample() { var b = IqcInventoryBatchBuilder.Build(Receipt(100m, 10m)); var bal = new InventoryBalance(); foreach (var c in b.Commands) bal = InventoryBalanceCalculator.Project(bal, c).After; Assert.Equal(90m, bal.QtyOnHand); // +100 -10 Assert.Equal(90m, bal.AvailStatusQty); Assert.Equal(0m, bal.Assay); } }