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");
}
}