using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
///
/// C5 库存量变化契约测试(Phase 5A;纯计算,无 DB)。
/// 精确固化 pr_SFM_InventoryTransactionProcessing2 各 TransType 的 ΔQtyOnHand/ΔAvail/ΔAssay/ΔFreeze/ΔQtyOnOrd。
///
public class InventoryBalanceCalculatorTests
{
private static InventoryTransactionCommand Cmd(string transType, decimal changeQty, decimal umc = 1m)
=> new InventoryTransactionCommand
{
TransType = transType, ChangeQty = changeQty, UmConversion = umc,
ItemNum = "IT-1", Location = "L1",
};
// TransType | ChangeQty | umc | conv | ΔQtyOnHand | ΔAvail | ΔAssay | ΔFreeze | ΔQtyOnOrd
[Theory]
// 采购收货入质检库存(IQC 合格入库核心):在手+、质检+、可用不变
[InlineData("rct-po-ins", 100, 1, 100, 100, 0, 100, 0, 0)]
// 质检转出(ChangeQty<0 走 Assay 掩码):在手-、质检-
[InlineData("iss-tr-ins", -30, 1, -30, -30, 0, -30, 0, 0)]
// 质检相关入(ChangeQty>0 不走 Assay 掩码):在手+、可用+
[InlineData("iss-tr-ins", 30, 1, 30, 30, 30, 0, 0, 0)]
// 冻结:可用-、冻结+(ΔFreeze=-conv)
[InlineData("inv-frz", -50, 1, -50, 0, -50, 0, 50, 0)]
// 解冻:按 LIVE 同式(DB_PENDING 符号)
[InlineData("inv-unfrz", -50, 1, -50, 0, -50, 0, 50, 0)]
// 销售出库:在手-、可用-、在订-
[InlineData("iss-so", -20, 1, -20, -20, -20, 0, 0, -20)]
// 一般出入库(其它 TransType):在手/可用同步
[InlineData("rct-x-ins", 40, 1, 40, 40, 40, 0, 0, 0)]
// UMConversion≠1:conv=ChangeQty×umc
[InlineData("rct-po-ins", 10, 2, 20, 20, 0, 20, 0, 0)]
public void ComputeRowDeltas_PerTransType_MatchLiveFormula(
string tt, double changeQty, double umc, double conv,
double dQoh, double dAvail, double dAssay, double dFreeze, double dOrd)
{
var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd(tt, (decimal)changeQty, (decimal)umc));
Assert.Equal((decimal)conv, d.Conv);
Assert.Equal((decimal)dQoh, d.DeltaQtyOnHand);
Assert.Equal((decimal)dAvail, d.DeltaAvailStatusQty);
Assert.Equal((decimal)dAssay, d.DeltaAssay);
Assert.Equal((decimal)dFreeze, d.DeltaFreezeQty);
Assert.Equal((decimal)dOrd, d.DeltaQtyOnOrd);
}
[Fact]
public void ComputeRowDeltas_UmConversionZero_NormalizedToOne()
{
var d = InventoryBalanceCalculator.ComputeRowDeltas(Cmd("rct-po-ins", 7m, 0m));
Assert.Equal(7m, d.Conv); // umc 0 → 1
Assert.Equal(7m, d.DeltaAssay);
}
[Fact]
public void Project_BeforeToAfter_RctPoIns_IncreasesOnHandAndAssayOnly()
{
var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 80m, Assay = 20m, FreezeQty = 0m, QtyOnOrd = 0m };
var (d, after) = InventoryBalanceCalculator.Project(before, Cmd("rct-po-ins", 50m));
Assert.Equal(50m, d.Conv);
Assert.Equal(150m, after.QtyOnHand); // 在手 +50
Assert.Equal(80m, after.AvailStatusQty); // 可用不变(进质检)
Assert.Equal(70m, after.Assay); // 质检 +50
Assert.Equal(0m, after.FreezeQty);
// 不 mutate 入参
Assert.Equal(100m, before.QtyOnHand);
}
[Fact]
public void Project_Freeze_MovesAvailToFreeze()
{
var before = new InventoryBalance { QtyOnHand = 100m, AvailStatusQty = 100m, Assay = 0m, FreezeQty = 0m };
var (_, after) = InventoryBalanceCalculator.Project(before, Cmd("inv-frz", -40m));
Assert.Equal(100m, after.QtyOnHand); // 在手不变
Assert.Equal(60m, after.AvailStatusQty); // 可用 -40
Assert.Equal(40m, after.FreezeQty); // 冻结 +40
}
[Fact]
public void BuildTransHist_RctPoIns_FieldsMatchLive()
{
var cmd = Cmd("rct-po-ins", 100m);
cmd.Price = 5m;
var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, beginBalance: 100m);
Assert.Equal(100m, h.BeginBalance);
Assert.Equal(100m, h.QtyChange); // 非 frz → conv
Assert.Equal(0m, h.FreezeQty);
Assert.Equal(100m, h.Assay);
Assert.Equal(500m, h.Amt); // conv × Price
Assert.Equal("rct-po-ins", h.TransType);
}
[Fact]
public void BuildTransHist_FreezeTrans_QtyChangeZero()
{
var cmd = Cmd("inv-frz", -50m);
var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m);
Assert.Equal(0m, h.QtyChange); // frz/unfrz 流水 QtyChange=0
Assert.Equal(50m, h.FreezeQty);
}
[Fact]
public void BuildTransHist_QcCheckRemark_UsesReasonAsTransType()
{
var cmd = Cmd("rct-po-ins", 10m);
cmd.Remark = "QcCheck";
cmd.Reason = "iqc-pass";
var d = InventoryBalanceCalculator.ComputeRowDeltas(cmd);
var h = InventoryBalanceCalculator.BuildTransHist(cmd, d, 0m);
Assert.Equal("iqc-pass", h.TransType); // Remark=='QcCheck' → 取 Reason
}
[Fact]
public void ComputeRowDeltas_NullCommand_Throws()
{
Assert.Throws(() => InventoryBalanceCalculator.ComputeRowDeltas(null));
}
}