| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess.Providers;
- using Microsoft.Extensions.Logging.Abstractions;
- using Xunit;
- namespace Admin.NET.Test.S8;
- /// <summary>
- /// S8-RULE01-PURCHASE-DELIVERY-A:PURCHASE_DELIVERY 的取数作用域、canonical 映射与 TIMEOUT 判定契约。
- /// 锁定项与失败后果:
- ///
- /// A. 作用域只能按 tenant → Phase 1 拍板:Security Boundary = Tenant。若有人日后往 SQL 里补回
- /// factory_id 过滤,规则会恒返 0 行且不报错(dwd_supplier_delivery.factory_id
- /// 是写死的常量 1,与 S8 侧真实 SysOrg.Id 永不相等)——静默失效,最难发现。
- /// B. 快照按 tenant 各自收敛 → 用全局 MAX(stat_date) 会让落后一天的租户整租户静默丢数据。
- /// C. 身份稳定且唯一 → source_object_id 进 DedupKey,格式一变历史异常与新异常直接断代。
- /// D. DedupKey 预算守卫 → 预算比 source_object_id 列宽(64)更紧;落在预算与列宽之间的键
- /// 会过得了列校验、卡在 DedupKey 上,数据库静默截断会把两个 PO Line 折叠成一条异常。
- /// E. due_at 双分支 → need_date 为 NULL 时回落 due_date;两者实测 18/36 行不同,不是等价列。
- /// F. 判定归 evaluator → Provider 不滤 status,"逾期但已完成不命中"必须由 evaluator 判出来。
- /// G. 取数路径唯一 → Provider 取数失败必须暴露为 dataset_query_failed,而不是换一条路。
- ///
- /// 不接 DB、不接 DI、不触发任何真实规则。SQL 在真实库上的行为已由实施前的只读核验覆盖
- /// (4 租户身份唯一、跨租户交集 0、外层走 idx_stat_date),本文件锁定的是结构契约。
- /// </summary>
- public class S8PurchaseDeliveryProviderMappingTests
- {
- private const string PurchaseDelivery = S8BusinessDatasetDefinitions.PurchaseDeliveryCode;
- private const string RuleCode = "RULE_S4_PURCHASE_DELIVERY_DATE_DELAY";
- private static S8MonitoringDataRequest Request(
- long tenantId = 838257186181189, long factoryId = 838257186320453) => new()
- {
- TenantId = tenantId,
- FactoryId = factoryId,
- DatasetCode = PurchaseDelivery,
- RuleCode = RuleCode,
- RuleType = "TIMEOUT"
- };
- private static AdoS8WatchRule NewRule() => new()
- {
- Id = 900101,
- RuleCode = RuleCode,
- SceneCode = "S4",
- WatchObjectType = "PURCHASE_ORDER_LINE",
- SourceObjectType = "PURCHASE_ORDER_LINE",
- Severity = "SERIOUS",
- RuleType = "TIMEOUT",
- DatasetCode = PurchaseDelivery
- };
- private static S8TimeoutParams Params() => S8TimeoutParams.Parse(
- """
- {"dueAtField":"due_at","statusField":"status",
- "completedStates":["COMPLETED","CLOSED","CANCELLED"],
- "objectCodeField":"related_object_code","objectIdField":"source_object_id",
- "graceMinutes":0,"exceptionTypeCode":"PURCHASE_DELIVERY_ABNORMAL"}
- """);
- /// <summary>真实 DWD 行样本(取自 UAT 租户 2026-09-05 快照)。</summary>
- private static S8PurchaseDeliveryDataProvider.PurchaseDeliveryRow Row(
- string poNo, string poLine, DateTime? needDate, DateTime? dueDate, string status,
- string supplierName = "珩睿", string supplierCode = "10003232") => new()
- {
- SourceObjectId = $"{poNo}#{poLine}",
- RelatedObjectCode = poNo,
- RelatedObjectName = string.IsNullOrEmpty(supplierName) ? supplierCode : supplierName,
- DueAt = needDate ?? dueDate,
- Status = status,
- DueAtSource = needDate == null ? "due_date" : "need_date",
- PoNo = poNo,
- PoLine = poLine,
- PoType = "po",
- SupplierCode = supplierCode,
- SupplierName = supplierName,
- ItemCode = "ITM-001",
- ItemName = "物料",
- OrderQty = 3000m,
- RemainingQty = 3000m,
- NeedDate = needDate,
- DueDate = dueDate,
- RiskLevel = "HIGH",
- StatDate = new DateTime(2026, 9, 5)
- };
- private static S8MonitoringRowSet RowSet(params S8PurchaseDeliveryDataProvider.PurchaseDeliveryRow[] rows) =>
- S8MonitoringRowSet.FromRows(
- rows.Select(r => S8MonitoringRow.FromValues(S8PurchaseDeliveryDataProvider.ToCanonicalValues(r))));
- // ============================================================
- // A/B. 作用域与快照收敛(SQL 结构契约)
- // ============================================================
- [Fact]
- public void T4_Sql_FiltersByTenant_InBothOuterQueryAndSnapshotSubquery()
- {
- // 外层与快照子查询都必须带租户,否则会拿到别的租户的最新快照日期。
- var sql = S8PurchaseDeliveryDataProvider.Sql;
- Assert.Contains("d.tenant_id = @TenantId", sql);
- Assert.Contains("x.tenant_id = @TenantId", sql);
- }
- [Fact]
- public void T5_Sql_DoesNotUseFactoryAsDatasetFilter()
- {
- // Phase 1 拍板:Factory 是业务上下文,不是本数据集的过滤条件。
- // 这条断言是防回归的主锁——补回 factory 过滤会让规则恒返 0 行且不报任何错。
- Assert.DoesNotContain("factory_id", S8PurchaseDeliveryDataProvider.Sql);
- }
- [Fact]
- public void T5b_FactoryId_DoesNotAlterQuery_ButTenantIdDoes()
- {
- // 同一租户下换工厂:SQL 与唯一参数都不受影响(工厂根本不进 SQL)。
- // 换租户:参数随之改变。二者共同证明"隔离维度只有租户"。
- var sameTenantOtherFactory = Request(factoryId: 999_999_999);
- var otherTenant = Request(tenantId: 838257212780613);
- Assert.Equal(Request().TenantId, sameTenantOtherFactory.TenantId);
- Assert.NotEqual(Request().TenantId, otherTenant.TenantId);
- // 工厂不在 SQL 中,因此不可能成为取数差异的来源。
- Assert.DoesNotContain("@FactoryId", S8PurchaseDeliveryDataProvider.Sql);
- }
- [Fact]
- public void T6_Sql_ConvergesSnapshot_PerTenantLatest_NotGlobalMax()
- {
- var sql = S8PurchaseDeliveryDataProvider.Sql;
- // 反向索引扫描而非聚合:大租户上实测 143ms → 2.7ms。
- Assert.Contains("ORDER BY x.stat_date DESC", sql);
- Assert.Contains("LIMIT 1", sql);
- Assert.DoesNotContain("MAX(", sql);
- }
- [Fact]
- public void Sql_ExcludesDegenerateIdentity_AndDoesNotFilterOnBusinessState()
- {
- var sql = S8PurchaseDeliveryDataProvider.Sql;
- // 身份任一段为空会产生退化的 "#",必须排除。
- Assert.Contains("d.po_no <> ''", sql);
- Assert.Contains("d.po_line <> ''", sql);
- // 判定归 evaluator:Provider 不得按状态或不可信的 remaining_qty 预筛。
- Assert.DoesNotContain("remaining_qty >", sql);
- Assert.DoesNotContain("delivery_status <>", sql);
- Assert.DoesNotContain("delivery_status !=", sql);
- }
- [Fact]
- public async Task LoadAsync_ValidatesScope_BeforeTouchingDatabase()
- {
- // 故意注入 null 数据库:若作用域校验不是第一步,这里会得到 NullReferenceException。
- var provider = new S8PurchaseDeliveryDataProvider(
- null, NullLogger<S8PurchaseDeliveryDataProvider>.Instance);
- var ex = await Assert.ThrowsAsync<S8RuleEvaluatorException>(() =>
- provider.LoadAsync(Request(tenantId: 0)));
- Assert.Equal("dataset_scope_invalid", ex.Reason);
- var ex2 = await Assert.ThrowsAsync<S8RuleEvaluatorException>(() =>
- provider.LoadAsync(Request(factoryId: 0)));
- Assert.Equal("dataset_scope_invalid", ex2.Reason);
- }
- // ============================================================
- // C/D. 身份与 DedupKey 预算
- // ============================================================
- [Fact]
- public void T7_Identity_IsStableComposite_UniquePerLine()
- {
- var line3 = Row("PO202608260002", "3", new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED");
- var line4 = Row("PO202608260002", "4", new DateTime(2026, 8, 17), new DateTime(2026, 9, 10), "DELAYED");
- var v3 = S8PurchaseDeliveryDataProvider.ToCanonicalValues(line3);
- var v4 = S8PurchaseDeliveryDataProvider.ToCanonicalValues(line4);
- Assert.Equal("PO202608260002#3", v3[S8CanonicalColumns.SourceObjectId]);
- Assert.Equal("PO202608260002", v3[S8CanonicalColumns.RelatedObjectCode]);
- // 同单不同行必须是不同对象,否则同一采购单只会产出一条异常。
- Assert.NotEqual(v3[S8CanonicalColumns.SourceObjectId], v4[S8CanonicalColumns.SourceObjectId]);
- }
- [Fact]
- public void T7b_Identity_IsInvariantAcrossSnapshots()
- {
- // 身份只由 po_no + po_line 决定,与 stat_date 无关;否则每天换一次身份,
- // 昨天的异常今天会被判恢复、并新建一条。
- var day1 = Row("PO202608260002", "3", new DateTime(2026, 8, 17), new DateTime(2026, 9, 10), "DELAYED");
- var day2 = Row("PO202608260002", "3", new DateTime(2026, 8, 17), new DateTime(2026, 9, 10), "DELAYED");
- day2.StatDate = new DateTime(2026, 9, 6);
- Assert.Equal(
- S8PurchaseDeliveryDataProvider.ToCanonicalValues(day1)[S8CanonicalColumns.SourceObjectId],
- S8PurchaseDeliveryDataProvider.ToCanonicalValues(day2)[S8CanonicalColumns.SourceObjectId]);
- }
- [Fact]
- public void T7c_DedupKey_StaysWithinColumnLimit_ForRealWorstCase()
- {
- // 实测该租户最长 source_object_id 为 16 字符;此处按真实规则编码与作用域算完整 DedupKey。
- var key = S8TimeoutRuleEvaluator.BuildDedupKey(
- 838257186181189, 838257186320453, RuleCode, "PURCHASE_ORDER_LINE", "PO202608260002#4");
- Assert.True(key.Length <= S8PurchaseDeliveryDataProvider.DedupKeyMaxLength,
- $"DedupKey 长度 {key.Length} 超过 {S8PurchaseDeliveryDataProvider.DedupKeyMaxLength}:{key}");
- }
- [Fact]
- public void T7d_Budget_IsTighterThanSourceObjectIdColumnWidth()
- {
- // 关键认知:预算(128 − 前缀)比 source_object_id 的列宽 64 更紧。
- // 落在预算与 64 之间的键会过得了列校验、卡在 DedupKey 上。
- var budget = S8PurchaseDeliveryDataProvider.ResolveSourceObjectIdBudget(Request());
- Assert.True(budget > 0);
- Assert.True(budget < 64, $"预期预算紧于列宽 64,实际 {budget}");
- // 预算 + 前缀必须正好等于上限,证明算式与 BuildDedupKey 的格式一致。
- var probe = new string('x', budget);
- var key = S8TimeoutRuleEvaluator.BuildDedupKey(
- Request().TenantId, Request().FactoryId, RuleCode, "PURCHASE_ORDER_LINE", probe);
- Assert.Equal(S8PurchaseDeliveryDataProvider.DedupKeyMaxLength, key.Length);
- }
- [Fact]
- public void T7e_Budget_ShrinksWhenRuleCodeGrows()
- {
- // 预算按 request 现算而非写死;换更长的规则编码必须自动收紧。
- var longRule = new S8MonitoringDataRequest
- {
- TenantId = Request().TenantId,
- FactoryId = Request().FactoryId,
- DatasetCode = PurchaseDelivery,
- RuleCode = RuleCode + "_MUCH_LONGER_SUFFIX",
- RuleType = "TIMEOUT"
- };
- Assert.True(
- S8PurchaseDeliveryDataProvider.ResolveSourceObjectIdBudget(longRule)
- < S8PurchaseDeliveryDataProvider.ResolveSourceObjectIdBudget(Request()));
- }
- // ============================================================
- // E. Canonical 映射
- // ============================================================
- [Fact]
- public void T8_Canonical_Mapping_IsComplete_WhenNeedDatePresent()
- {
- var values = S8PurchaseDeliveryDataProvider.ToCanonicalValues(
- Row("PO202608260002", "3", new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
- Assert.Equal("PO202608260002#3", values[S8CanonicalColumns.SourceObjectId]);
- Assert.Equal("PO202608260002", values[S8CanonicalColumns.RelatedObjectCode]);
- Assert.Equal(new DateTime(2026, 8, 17, 4, 33, 7), values[S8CanonicalColumns.DueAt]);
- Assert.Equal("DELAYED", values[S8CanonicalColumns.Status]);
- Assert.Equal("need_date", values["due_at_source"]);
- }
- [Fact]
- public void T8b_DueAt_FallsBackToDueDate_WhenNeedDateIsNull()
- {
- // 真实样本 PW202608280001#1:need_date 为 NULL,是唯一能验回落分支的行。
- var values = S8PurchaseDeliveryDataProvider.ToCanonicalValues(
- Row("PW202608280001", "1", null, new DateTime(2026, 8, 29), "DELAYED", supplierName: "怡祥"));
- Assert.Equal(new DateTime(2026, 8, 29), values[S8CanonicalColumns.DueAt]);
- // due_at 是两种业务日期的混合,必须逐行自证用的是哪个基准。
- Assert.Equal("due_date", values["due_at_source"]);
- }
- [Fact]
- public void T8c_Canonical_DoesNotFabricateBusinessJudgements()
- {
- // Provider 不得构造 is_delivery_complete / is_active_obligation / severity ——
- // 上一轮数据契约审计已判这三项 NOT_PROVEN,凭空构造等于把中台缺口埋进 S8。
- var values = S8PurchaseDeliveryDataProvider.ToCanonicalValues(
- Row("PO202608260002", "1", new DateTime(2026, 8, 17), new DateTime(2026, 9, 10), "COMPLETED"));
- Assert.DoesNotContain("is_delivery_complete", values.Keys);
- Assert.DoesNotContain("is_active_obligation", values.Keys);
- Assert.DoesNotContain("severity", values.Keys);
- }
- // ============================================================
- // F. TIMEOUT 判定(由 evaluator 判,不由 Provider 预筛)
- // ============================================================
- [Fact]
- public void T9_Timeout_Hits_WhenOverdueAndNotCompleted()
- {
- var rowSet = RowSet(Row("PO202608260002", "3",
- new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
- var hits = S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- var hit = Assert.Single(hits);
- Assert.Equal("PO202608260002#3", hit.SourceObjectId);
- Assert.Equal("PO202608260002", hit.RelatedObjectCode);
- Assert.Equal("PURCHASE_ORDER_LINE", hit.SourceObjectType);
- }
- [Fact]
- public void T10_Timeout_NoHit_WhenNotYetDue()
- {
- // 真实样本 PO202608280001#1:due 2026-09-17,状态 OPEN。
- var rowSet = RowSet(Row("PO202608280001", "1",
- new DateTime(2026, 9, 17), new DateTime(2026, 8, 9), "OPEN", supplierName: "怡祥"));
- var hits = S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- Assert.Empty(hits);
- }
- [Fact]
- public void T11_Timeout_NoHit_WhenOverdueButCompleted()
- {
- // 真实样本 PO202608260002#1:已逾期但 COMPLETED。
- // 这一条必须由 evaluator 判出来 —— 若 Provider 在 SQL 里滤掉已完成行,
- // 本测试会"通过"但测的是过滤而非判定,且该行不在 hits 里会被误判为恢复。
- var rowSet = RowSet(Row("PO202608260002", "1",
- new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "COMPLETED"));
- var hits = S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- Assert.Empty(hits);
- }
- [Fact]
- public void T11b_Timeout_NoHit_ForClosedAndCancelled()
- {
- // dwd_supplier_delivery 有三个写入方,词表不一致:批量转换产 COMPLETED/DELAYED/OPEN,
- // 增量写入方产 CANCELLED/CLOSED/PARTIAL。当前库中后两值 0 行,这是零行为变更的安全余量。
- var rowSet = RowSet(
- Row("PO-CLOSED", "1", new DateTime(2026, 8, 1), null, "CLOSED"),
- Row("PO-CANCELLED", "1", new DateTime(2026, 8, 1), null, "CANCELLED"));
- var hits = S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- Assert.Empty(hits);
- }
- [Fact]
- public void T11c_Timeout_Hits_ForPartial_BecausePartialIsNotTerminal()
- {
- // PARTIAL 刻意不列入 completedStates:部分收货不是终态,仍需继续监控。
- var rowSet = RowSet(Row("PO-PARTIAL", "1", new DateTime(2026, 8, 1), null, "PARTIAL"));
- var hits = S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- Assert.Single(hits);
- }
- [Fact]
- public void T13_Recovery_Contract_HitSetShrinksWhenStatusBecomesCompleted()
- {
- // 恢复判定的输入是"本轮 hits 的 dedup_key 集合"(见 ReconcileRecoveriesForRuleAsync)。
- // 这里证明:同一对象在状态转为 COMPLETED 后不再出现在 hits 中,
- // 从而进入恢复候选。真实 recovered_at 写入属运行态,由 Batch B 验证。
- var before = RowSet(Row("PO202608260002", "2",
- new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
- var after = RowSet(Row("PO202608260002", "2",
- new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "COMPLETED"));
- var hitsBefore = S8TimeoutRuleEvaluator.EvaluateRows(
- before, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- var hitsAfter = S8TimeoutRuleEvaluator.EvaluateRows(
- after, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
- var dedupKey = Assert.Single(hitsBefore).DedupKey;
- Assert.Empty(hitsAfter);
- Assert.DoesNotContain(dedupKey, hitsAfter.Select(h => h.DedupKey));
- }
- [Fact]
- public void T14_Hit_CarriesExceptionTypeAndDiagnosticPayload()
- {
- var rowSet = RowSet(Row("PO202608260002", "3",
- new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
- var hit = Assert.Single(S8TimeoutRuleEvaluator.EvaluateRows(
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5)));
- Assert.Equal("PURCHASE_DELIVERY_ABNORMAL", hit.ExceptionTypeCode);
- Assert.Equal("PURCHASE_ORDER_LINE", hit.SourceObjectType);
- Assert.Equal("PO202608260002#3", hit.SourceObjectId);
- Assert.Equal("T838257186181189:F838257186320453:RRULE_S4_PURCHASE_DELIVERY_DATE_DELAY:PURCHASE_ORDER_LINE:PO202608260002#3",
- hit.DedupKey);
- // 诊断列必须随 payload 落盘:remaining_qty 供后续数据治理观察,
- // due_at_source 用于追溯每行实际使用了 need_date 还是 due_date。
- Assert.Contains("remaining_qty", hit.SourcePayload);
- Assert.Contains("due_at_source", hit.SourcePayload);
- Assert.Contains("stat_date", hit.SourcePayload);
- }
- // ============================================================
- // G. 取数路径唯一
- // ============================================================
- //
- // 原 T12 / T12b 断言「STANDARD_DATASET 绝不回落 Legacy」,做法是注入一个
- // SpyLegacy 探针并断言 CallCount == 0,同时在 rule.Expression 里塞一条
- // "DROP TABLE ado_s8_exception; SELECT SHOULD_NEVER_RUN" 当诱饵。
- //
- // S8-STANDARD-DATASET-HARD-CUTOVER-1 之后这两条已无法成立、也无需成立:
- // · IS8LegacySqlDataProvider 不存在 → 探针类型写不出来;
- // · AdoS8WatchRule.Expression / DataSourceId 不存在 → 诱饵放不进去。
- // 「不回落」从一条需要反复验证的运行期行为,变成了类型系统层面的事实。
- //
- // 结构性替代断言见 S8MonitoringDataGatewayTests.Gateway_HasNoLegacyDependency
- // 与 WatchRule_HasNoSqlOrDataSourceFields;此处只保留失败分类这一条仍有意义的行为。
- [Fact]
- public async Task T12_ProviderFailure_SurfacesAsQueryFailed()
- {
- var gateway = new S8MonitoringDataGateway(
- new S8MonitoringDataProviderRegistry(new IS8MonitoringDataProvider[] { new ThrowingPurchaseDeliveryProvider() }),
- new S8DatasetCatalog(new IS8DatasetDefinitionSource[] { new S8BusinessDatasetDefinitions() }),
- NullLogger<S8MonitoringDataGateway>.Instance);
- var ex = await Assert.ThrowsAsync<S8RuleEvaluatorException>(() =>
- gateway.LoadAsync(838257186181189, 838257186320453, NewRule(), "TIMEOUT", 1000));
- Assert.Equal(S8DatasetReasonCode.QueryFailed, ex.Reason);
- }
- private sealed class EmptyPurchaseDeliveryProvider : IS8MonitoringDataProvider
- {
- public string DatasetCode => PurchaseDelivery;
- public S8DatasetCapabilities Capabilities => S8BusinessDatasetDefinitions.PurchaseDeliveryCapabilities;
- public Task<S8MonitoringRowSet> LoadAsync(
- S8MonitoringDataRequest request, CancellationToken cancellationToken = default)
- {
- request.EnsureValid();
- return Task.FromResult(S8MonitoringRowSet.Empty);
- }
- }
- private sealed class ThrowingPurchaseDeliveryProvider : IS8MonitoringDataProvider
- {
- public string DatasetCode => PurchaseDelivery;
- public S8DatasetCapabilities Capabilities => S8BusinessDatasetDefinitions.PurchaseDeliveryCapabilities;
- public Task<S8MonitoringRowSet> LoadAsync(
- S8MonitoringDataRequest request, CancellationToken cancellationToken = default) =>
- throw new InvalidOperationException("dwd unavailable");
- }
- }
|