| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- 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;
- /// <summary>可配置标准层读取(测试用,替代真实 mdp_std_iqc_receipt_state;无 DB)。</summary>
- public sealed class FakeIqcReceiptStdReader : IIqcReceiptStdReader
- {
- public IqcReceiptStdRow Row { get; set; }
- public Task<IqcReceiptStdRow> LoadRowAsync(long tenantId, string domainCode, string fbillno)
- => Task.FromResult(Row);
- }
- /// <summary>
- /// L0:MdpIqcReceiptStateLoader 字段保真(由 mdp_std 事实读时计算布尔)+ freshness 契约 + 双源无关。
- /// 全程 Fake std reader,无 DB(态①,实库贴源留 Phase 5B)。
- /// </summary>
- 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 解耦
- }
- }
|