MdpIqcReceiptStateLoaderTests.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L0;
  3. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L1;
  4. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
  5. using Xunit;
  6. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  7. /// <summary>可配置标准层读取(测试用,替代真实 mdp_std_iqc_receipt_state;无 DB)。</summary>
  8. public sealed class FakeIqcReceiptStdReader : IIqcReceiptStdReader
  9. {
  10. public IqcReceiptStdRow Row { get; set; }
  11. public Task<IqcReceiptStdRow> LoadRowAsync(long tenantId, string domainCode, string fbillno)
  12. => Task.FromResult(Row);
  13. }
  14. /// <summary>
  15. /// L0:MdpIqcReceiptStateLoader 字段保真(由 mdp_std 事实读时计算布尔)+ freshness 契约 + 双源无关。
  16. /// 全程 Fake std reader,无 DB(态①,实库贴源留 Phase 5B)。
  17. /// </summary>
  18. public class MdpIqcReceiptStateLoaderTests
  19. {
  20. private const long Tenant = 797403760988229L;
  21. private static IqcReceiptStdRow FullRow(DateTime? synced = null) => new IqcReceiptStdRow
  22. {
  23. TenantId = Tenant, Domain = "8010", Fbillno = "FB-1", QcNbr = "QC-1",
  24. ItemNum = "IT-1", Location = "L1", LotSerial = "", RctNbr = "RCT-1",
  25. PurOrd = "PO-1", PurLine = 3, Potype = "po",
  26. OrderedQty = 100m, ReceivedCumQty = 30m, ReturnedCumQty = 2m, SampleQty = 5m,
  27. ReceiptPendingQty = 8m, ReceivableDetailCount = 4, SourceSyncedAt = synced,
  28. };
  29. private static MdpIqcReceiptStateLoader Loader(IqcReceiptStdRow row)
  30. => new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = row });
  31. // L01:FBILLNO 找到完整状态 → 字段映射齐全
  32. [Fact]
  33. public async Task L01_Found_MapsAllFields()
  34. {
  35. var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
  36. Assert.True(st.Found);
  37. Assert.True(st.HasPendingReceipt); // pending 8>0
  38. Assert.True(st.HasReceivableDetails); // count 4>0
  39. Assert.Equal("IT-1", st.ItemNum);
  40. Assert.Equal("L1", st.Location);
  41. Assert.Equal("RCT-1", st.RctNbr);
  42. Assert.Equal("PO-1", st.PurOrd);
  43. Assert.Equal(3, st.PurLine);
  44. Assert.Equal("po", st.Potype);
  45. Assert.Equal("", st.Refs); // UNRESOLVED → 恒空
  46. }
  47. // L02:找不到 → Found=false(确定性)
  48. [Fact]
  49. public async Task L02_NotFound_FoundFalse()
  50. {
  51. var st = await Loader(null).LoadAsync(Tenant, "8010", "NOPE");
  52. Assert.False(st.Found);
  53. }
  54. // L03:HasPendingReceipt 由 receipt_pending_qty 计算(=0 → false)
  55. [Fact]
  56. public async Task L03_NoPending_HasPendingFalse()
  57. {
  58. var row = FullRow(); row.ReceiptPendingQty = 0m;
  59. var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
  60. Assert.True(st.Found);
  61. Assert.False(st.HasPendingReceipt);
  62. }
  63. // L04:无可收明细 → HasReceivableDetails=false
  64. [Fact]
  65. public async Task L04_NoReceivableDetail_False()
  66. {
  67. var row = FullRow(); row.ReceivableDetailCount = 0;
  68. var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
  69. Assert.False(st.HasReceivableDetails);
  70. }
  71. // L06:批次空 → LotSerial "" 且 Refs "" 保持
  72. [Fact]
  73. public async Task L06_EmptyLot_StaysEmpty()
  74. {
  75. var row = FullRow(); row.LotSerial = "";
  76. var st = await Loader(row).LoadAsync(Tenant, "8010", "FB-1");
  77. Assert.Equal("", st.LotSerial);
  78. Assert.Equal("", st.Refs);
  79. }
  80. // L07:SampleQty 正确(inspbill.lysl)
  81. [Fact]
  82. public async Task L07_SampleQty_Mapped()
  83. {
  84. var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
  85. Assert.Equal(5m, st.SampleQty);
  86. }
  87. // L08:PO 数量 Ordered/Received(累计)/Returned 正确
  88. [Fact]
  89. public async Task L08_PoQuantities_Mapped()
  90. {
  91. var st = await Loader(FullRow()).LoadAsync(Tenant, "8010", "FB-1");
  92. Assert.Equal(100m, st.OrderedQty);
  93. Assert.Equal(30m, st.BeforeReceivedQty); // received_cum_qty
  94. Assert.Equal(2m, st.ReturnedQty);
  95. }
  96. // ── freshness(经消费者,需 event 时刻)──
  97. private const string Dom = "8010";
  98. private static readonly DateTime EvtTime = new DateTime(2026, 8, 9);
  99. private static (FakeInventoryDatabase db, InMemoryIqcPostingStore store, IqcInventoryEventConsumer consumer) Rig(IqcReceiptStdRow row)
  100. {
  101. var db = new FakeInventoryDatabase();
  102. db.SeedMaster(Tenant, Dom, "IT-1", "L1");
  103. var store = new InMemoryIqcPostingStore();
  104. var idem = new IqcPostingIdempotencyService(store);
  105. var factory = new FakeIqcReceiptUnitOfWorkFactory(db);
  106. var loader = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = row });
  107. var consumer = new IqcInventoryEventConsumer(
  108. idem, new InMemoryBusinessCompletionStore(db), loader,
  109. new IqcReceiptOrchestrator(factory), new IqcMrbSelectionTaskService(factory));
  110. return (db, store, consumer);
  111. }
  112. private static IqcInventoryEvent Evt() => new IqcInventoryEvent
  113. {
  114. TenantId = Tenant, DomainCode = Dom, Fbillno = "FB-1", Receiver = "RCV-1", RctQcNbr = "QC-1",
  115. Pd = 0, Clfs = "", Dhsl = 100m, Bhgsl = 0m, User = "t", EventTime = EvtTime,
  116. };
  117. // L09:标准层水位落后于事件 → Retryable、0 C2
  118. [Fact]
  119. public async Task L09_FreshnessStale_Retryable_NoPosting()
  120. {
  121. var (db, _, consumer) = Rig(FullRow(synced: EvtTime.AddDays(-1))); // 同步早于事件
  122. var r = await consumer.ConsumeAsync(Evt());
  123. Assert.Equal(IqcConsumeOutcome.Retryable, r.Outcome);
  124. Assert.Equal(0, db.TotalPersisted);
  125. }
  126. // L10:标准层水位不落后 → 允许进入并过账
  127. [Fact]
  128. public async Task L10_FreshnessCurrent_Processes()
  129. {
  130. var (db, _, consumer) = Rig(FullRow(synced: EvtTime.AddDays(1))); // 同步不早于事件
  131. var r = await consumer.ConsumeAsync(Evt());
  132. Assert.Equal(IqcConsumeOutcome.Processed, r.Outcome);
  133. Assert.Equal(1, db.InvMaster.Count);
  134. }
  135. // L11:双源配置契约 —— 切换 mdp source 不改 loader 代码:
  136. // 同一 MdpIqcReceiptStateLoader 面对两个不同来源的 std reader,产出等价 IqcReceiptState。
  137. [Fact]
  138. public async Task L11_DualSource_LoaderCodeUnchanged()
  139. {
  140. var sourceA = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = FullRow() }); // 本库 aidopdev
  141. var sourceB = new MdpIqcReceiptStateLoader(new FakeIqcReceiptStdReader { Row = FullRow() }); // dopdemorq SQLSERVER
  142. var a = await sourceA.LoadAsync(Tenant, "8010", "FB-1");
  143. var b = await sourceB.LoadAsync(Tenant, "8010", "FB-1");
  144. Assert.Equal(a.PurOrd, b.PurOrd);
  145. Assert.Equal(a.OrderedQty, b.OrderedQty);
  146. Assert.Equal(a.HasPendingReceipt, b.HasPendingReceipt);
  147. Assert.Equal(a.Found, b.Found); // 读端与 source 解耦
  148. }
  149. }