using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// C5 9 道前置守卫正反测试(Phase 5A;纯校验,无 DB)。 /// LIVE 实核为 9 道;旧方案文档「8 道」为不完整总结(遗漏 G8 iss-tr-ins 质检过账≤质检库存)。 /// 实施以 pr_SFM_InventoryTransactionProcessing2 为最终契约。 /// public class InventoryGuardValidatorTests { private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1", string lot = "", string refs = "") => new InventoryTransactionCommand { TransType = tt, ChangeQty = qty, ItemNum = item, Location = loc, LotSerial = lot, Refs = refs }; private static InventoryContext Ctx( bool lotControl = false, bool dup = false, decimal avail = 1000m, decimal freeze = 1000m, decimal assay = 1000m, bool withItem = true, bool withLoc = true, bool withInvMaster = true, bool withDetail = true, string item = "IT-1", string loc = "L1", string lot = "", string refs = "") { var ctx = new InventoryContext { IsLotControl = lotControl, HasDuplicateBaseData = dup }; if (withItem) ctx.Items.Add(item); if (withLoc) ctx.Locations.Add(loc); if (withInvMaster) ctx.InvMasters[InventoryContext.InvKey(item, loc)] = new InventoryBalance { QtyOnHand = 1000m, AvailStatusQty = avail, Assay = assay, FreezeQty = freeze }; if (withDetail) { var k = lotControl ? InventoryContext.DetailKey(item, loc, lot, refs) : InventoryContext.DetailKey(item, loc, "", ""); ctx.LocationDetails[k] = new InventoryBalance { QtyOnHand = 1000m, AvailStatusQty = avail, Assay = assay, FreezeQty = freeze }; } return ctx; } private static List L(params InventoryTransactionCommand[] c) => c.ToList(); [Fact] public void G1_DuplicateBaseData_Fails() { var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(dup: true)); Assert.Single(e); Assert.Equal(InventoryGuardCode.DuplicateBaseData, e[0].Code); } [Fact] public void G2_ItemMasterMissing_Fails_Present_Passes() { var bad = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withItem: false)); Assert.Contains(bad, x => x.Code == InventoryGuardCode.ItemMasterMissing); var ok = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx()); Assert.Empty(ok); } [Fact] public void G3_LocationMasterMissing_Fails() { var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withLoc: false)); Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationMasterMissing); } [Fact] public void G4_InvMasterMissing_OnNetOutbound_Fails() { // 净出库 且 无 InvMaster var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10)), Ctx(withInvMaster: false, withDetail: false)); Assert.Contains(e, x => x.Code == InventoryGuardCode.InvMasterMissing); // 入库则不触发 G4 var ok = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 10)), Ctx(withInvMaster: false, withDetail: false)); Assert.DoesNotContain(ok, x => x.Code == InventoryGuardCode.InvMasterMissing); } [Fact] public void G5_LocationDetailMissing_OnNetOutbound_Fails() { var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10)), Ctx(withDetail: false)); Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationDetailMissing); } [Fact] public void G6_UnfreezeExceedsFreeze_Fails_Sufficient_Passes() { var bad = InventoryGuardValidator.Validate(L(Cmd("inv-unfrz", -100)), Ctx(freeze: 50m)); Assert.Contains(bad, x => x.Code == InventoryGuardCode.UnfreezeExceedsFreeze); var ok = InventoryGuardValidator.Validate(L(Cmd("inv-unfrz", -100)), Ctx(freeze: 200m)); Assert.Empty(ok); } [Fact] public void G7_FreezeExceedsAvail_Fails() { var e = InventoryGuardValidator.Validate(L(Cmd("inv-frz", -100)), Ctx(avail: 50m)); Assert.Contains(e, x => x.Code == InventoryGuardCode.FreezeExceedsAvail); } [Fact] public void G8_AssayIssueExceedsAssay_Fails() { // 旧总结遗漏的一道 var e = InventoryGuardValidator.Validate(L(Cmd("iss-tr-ins", -100)), Ctx(assay: 50m)); Assert.Contains(e, x => x.Code == InventoryGuardCode.AssayIssueExceedsAssay); } [Fact] public void G9_IssueExceedsAvail_Fails() { var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -100)), Ctx(avail: 50m)); Assert.Contains(e, x => x.Code == InventoryGuardCode.IssueExceedsAvail); } [Fact] public void AllGuardsPass_RctPoInsInbound_NoErrors() { var e = InventoryGuardValidator.Validate(L(Cmd("rct-po-ins", 100)), Ctx()); Assert.Empty(e); } [Fact] public void LotControl_G5_MissingBatch_IncludesLot() { var e = InventoryGuardValidator.Validate( L(Cmd("iss-x", -10, lot: "LOT-A")), Ctx(lotControl: true, withDetail: false, lot: "LOT-A")); var err = Assert.Single(e, x => x.Code == InventoryGuardCode.LocationDetailMissing); Assert.Equal("LOT-A", err.LotSerial); } [Fact] public void LotControl_DifferentLot_IsolatedStock() { // LOT-A 有库存、LOT-B 无 → 出 LOT-B 触发 G5,不受 LOT-A 影响 var ctx = Ctx(lotControl: true, withDetail: true, lot: "LOT-A"); var e = InventoryGuardValidator.Validate(L(Cmd("iss-x", -10, lot: "LOT-B")), ctx); Assert.Contains(e, x => x.Code == InventoryGuardCode.LocationDetailMissing && x.LotSerial == "LOT-B"); } }