using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L0;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L1;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
/// 可配置标准层读取(测试用,替代真实 mdp_std_iqc_receipt_state;无 DB)。
public sealed class FakeIqcReceiptStdReader : IIqcReceiptStdReader
{
public IqcReceiptStdRow Row { get; set; }
public Task LoadRowAsync(long tenantId, string domainCode, string fbillno)
=> Task.FromResult(Row);
}
///
/// L0:MdpIqcReceiptStateLoader 字段保真(由 mdp_std 事实读时计算布尔)+ freshness 契约 + 双源无关。
/// 全程 Fake std reader,无 DB(态①,实库贴源留 Phase 5B)。
///
public class MdpIqcReceiptStateLoaderTests
{
private const long Tenant = 797403760988229L;
private static IqcReceiptStdRow FullRow(DateTime? synced = null) => new IqcReceiptStdRow
{
TenantId = Tenant, Domain = "8010", Fbillno = "FB-1", QcNbr = "QC-1",
ItemNum = "IT-1", Location = "L1", LotSerial = "", RctNbr = "RCT-1",
PurOrd = "PO-1", PurLine = 3, Potype = "po",
OrderedQty = 100m, ReceivedCumQty = 30m, ReturnedCumQty = 2m, SampleQty = 5m,
ReceiptPendingQty = 8m, ReceivableDetailCount = 4, SourceSyncedAt = synced,
};
private static MdpIqcReceiptStateLoader Loader(IqcReceiptStdRow row)
=> new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = row });
// L01:FBILLNO 找到完整状态 → 字段映射齐全
[Fact]
public async Task L01_Found_MapsAllFields()
{
var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
Assert.True(st.Found);
Assert.True(st.HasPendingReceipt); // pending 8>0
Assert.True(st.HasReceivableDetails); // count 4>0
Assert.Equal("IT-1", st.ItemNum);
Assert.Equal("L1", st.Location);
Assert.Equal("RCT-1", st.RctNbr);
Assert.Equal("PO-1", st.PurOrd);
Assert.Equal(3, st.PurLine);
Assert.Equal("po", st.Potype);
Assert.Equal("", st.Refs); // UNRESOLVED → 恒空
}
// L02:找不到 → Found=false(确定性)
[Fact]
public async Task L02_NotFound_FoundFalse()
{
var st = await Loader(null).LoadAsync(Tenant, "8010", "NOPE");
Assert.False(st.Found);
}
// L03:HasPendingReceipt 由 receipt_pending_qty 计算(=0 → false)
[Fact]
public async Task L03_NoPending_HasPendingFalse()
{
var row = FullRow(); row.ReceiptPendingQty = 0m;
var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
Assert.True(st.Found);
Assert.False(st.HasPendingReceipt);
}
// L04:无可收明细 → HasReceivableDetails=false
[Fact]
public async Task L04_NoReceivableDetail_False()
{
var row = FullRow(); row.ReceivableDetailCount = 0;
var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
Assert.False(st.HasReceivableDetails);
}
// L06:批次空 → LotSerial "" 且 Refs "" 保持
[Fact]
public async Task L06_EmptyLot_StaysEmpty()
{
var row = FullRow(); row.LotSerial = "";
var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
Assert.Equal("", st.LotSerial);
Assert.Equal("", st.Refs);
}
// L07:SampleQty 正确(inspbill.lysl)
[Fact]
public async Task L07_SampleQty_Mapped()
{
var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
Assert.Equal(5m, st.SampleQty);
}
// L08:PO 数量 Ordered/Received(累计)/Returned 正确
[Fact]
public async Task L08_PoQuantities_Mapped()
{
var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
Assert.Equal(100m, st.OrderedQty);
Assert.Equal(30m, st.BeforeReceivedQty); // received_cum_qty
Assert.Equal(2m, st.ReturnedQty);
}
// ── freshness(经消费者,需 event 时刻)──
private const string Dom = "8010";
private static readonly DateTime EvtTime = new DateTime(2026, 8, 9);
private static (FakeInventoryDatabase db, InMemoryIqcPostingStore store, IqcInventoryEventConsumer consumer) Rig(IqcReceiptStdRow row)
{
var db = new FakeInventoryDatabase();
db.SeedMaster(Tenant, Dom, "IT-1", "L1");
var store = new InMemoryIqcPostingStore();
var idem = new IqcPostingIdempotencyService(store);
var factory = new FakeIqcReceiptUnitOfWorkFactory(db);
var loader = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = row });
var consumer = new IqcInventoryEventConsumer(
idem, new InMemoryBusinessCompletionStore(db), loader,
new IqcReceiptOrchestrator(factory), new IqcMrbSelectionTaskService(factory));
return (db, store, consumer);
}
private static IqcInventoryEvent Evt() => new IqcInventoryEvent
{
TenantId = Tenant, DomainCode = Dom, Fbillno = "FB-1", Receiver = "RCV-1", RctQcNbr = "QC-1",
Pd = 0, Clfs = "", Dhsl = 100m, Bhgsl = 0m, User = "t", EventTime = EvtTime,
};
// L09:标准层水位落后于事件 → Retryable、0 C2
[Fact]
public async Task L09_FreshnessStale_Retryable_NoPosting()
{
var (db, _, consumer) = Rig(FullRow(synced: EvtTime.AddDays(-1))); // 同步早于事件
var r = await consumer.ConsumeAsync(Evt());
Assert.Equal(IqcConsumeOutcome.Retryable, r.Outcome);
Assert.Equal(0, db.TotalPersisted);
}
// L10:标准层水位不落后 → 允许进入并过账
[Fact]
public async Task L10_FreshnessCurrent_Processes()
{
var (db, _, consumer) = Rig(FullRow(synced: EvtTime.AddDays(1))); // 同步不早于事件
var r = await consumer.ConsumeAsync(Evt());
Assert.Equal(IqcConsumeOutcome.Processed, r.Outcome);
Assert.Equal(1, db.InvMaster.Count);
}
// L11:双源配置契约 —— 切换 mdp source 不改 loader 代码:
// 同一 MdpIqcReceiptStateLoader 面对两个不同来源的 std reader,产出等价 IqcReceiptState。
[Fact]
public async Task L11_DualSource_LoaderCodeUnchanged()
{
var sourceA = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = FullRow() }); // 本库 aidopdev
var sourceB = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = FullRow() }); // dopdemorq SQLSERVER
var a = await sourceA.LoadAsync(Tenant, "8010", "FB-1");
var b = await sourceB.LoadAsync(Tenant, "8010", "FB-1");
Assert.Equal(a.PurOrd, b.PurOrd);
Assert.Equal(a.OrderedQty, b.OrderedQty);
Assert.Equal(a.HasPendingReceipt, b.HasPendingReceipt);
Assert.Equal(a.Found, b.Found); // 读端与 source 解耦
}
}