@spottoai/types-package 1.0.2-dev1138.1 → 1.0.2-dev1138.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/azure/financialAnalytics.d.ts +12 -6
- package/dist/azure/financialAnalytics.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDelivery.d.ts +70 -3
- package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDelivery.js +4 -1
- package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.d.ts +9 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.js +190 -10
- package/dist/azure/financialAnalyticsDeliveryValidation.js.map +1 -1
- package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsValidation.js +40 -12
- package/dist/azure/financialAnalyticsValidation.js.map +1 -1
- package/dist/azure/financialDataflow.d.ts +38 -2
- package/dist/azure/financialDataflow.d.ts.map +1 -1
- package/dist/azure/financialDataflow.js.map +1 -1
- package/dist/azure/financialDataflowKernel.d.ts +29 -0
- package/dist/azure/financialDataflowKernel.d.ts.map +1 -0
- package/dist/azure/financialDataflowKernel.js +117 -0
- package/dist/azure/financialDataflowKernel.js.map +1 -0
- package/dist/azure/financialDataflowValidation.d.ts +4 -1
- package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
- package/dist/azure/financialDataflowValidation.js +59 -4
- package/dist/azure/financialDataflowValidation.js.map +1 -1
- package/dist/azure/financialEvidenceAssessmentValidation.d.ts.map +1 -1
- package/dist/azure/financialEvidenceAssessmentValidation.js +7 -0
- package/dist/azure/financialEvidenceAssessmentValidation.js.map +1 -1
- package/dist/azure/financialPolicy.d.ts +16 -3
- package/dist/azure/financialPolicy.d.ts.map +1 -1
- package/dist/azure/financialPolicyDelivery.d.ts +75 -1
- package/dist/azure/financialPolicyDelivery.d.ts.map +1 -1
- package/dist/azure/financialPolicyDelivery.js +5 -1
- package/dist/azure/financialPolicyDelivery.js.map +1 -1
- package/dist/azure/financialPolicyDeliveryValidation.d.ts +16 -2
- package/dist/azure/financialPolicyDeliveryValidation.d.ts.map +1 -1
- package/dist/azure/financialPolicyDeliveryValidation.js +238 -5
- package/dist/azure/financialPolicyDeliveryValidation.js.map +1 -1
- package/dist/azure/financialPolicyKernel.d.ts +38 -0
- package/dist/azure/financialPolicyKernel.d.ts.map +1 -0
- package/dist/azure/financialPolicyKernel.js +232 -0
- package/dist/azure/financialPolicyKernel.js.map +1 -0
- package/dist/azure/financialPolicyValidation.d.ts +1 -0
- package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
- package/dist/azure/financialPolicyValidation.js +106 -16
- package/dist/azure/financialPolicyValidation.js.map +1 -1
- package/dist/azure/financialSavingsAuthority.d.ts +29 -0
- package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthority.js +2 -1
- package/dist/azure/financialSavingsAuthority.js.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.js +3 -0
- package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.js +36 -0
- package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
- package/dist/azure/financialScope.d.ts +2 -0
- package/dist/azure/financialScope.d.ts.map +1 -1
- package/dist/azure/financialScope.js +2 -0
- package/dist/azure/financialScope.js.map +1 -1
- package/dist/azure/financialScopeBaseline.d.ts +8 -2
- package/dist/azure/financialScopeBaseline.d.ts.map +1 -1
- package/dist/azure/financialScopeBaseline.js +14 -1
- package/dist/azure/financialScopeBaseline.js.map +1 -1
- package/dist/azure/financialScopeBaselineValidation.d.ts.map +1 -1
- package/dist/azure/financialScopeBaselineValidation.js +15 -1
- package/dist/azure/financialScopeBaselineValidation.js.map +1 -1
- package/dist/azure/financialScopeEvidence.d.ts +1 -1
- package/dist/azure/financialScopeEvidence.d.ts.map +1 -1
- package/dist/azure/views.d.ts +14 -3
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js.map +1 -1
- package/dist/esm/azure/financialAnalyticsDelivery.js +3 -0
- package/dist/esm/azure/financialAnalyticsDeliveryValidation.js +184 -11
- package/dist/esm/azure/financialAnalyticsValidation.js +40 -12
- package/dist/esm/azure/financialDataflowKernel.js +111 -0
- package/dist/esm/azure/financialDataflowValidation.js +55 -3
- package/dist/esm/azure/financialEvidenceAssessmentValidation.js +7 -0
- package/dist/esm/azure/financialPolicyDelivery.js +4 -0
- package/dist/esm/azure/financialPolicyDeliveryValidation.js +227 -7
- package/dist/esm/azure/financialPolicyKernel.js +225 -0
- package/dist/esm/azure/financialPolicyValidation.js +105 -16
- package/dist/esm/azure/financialSavingsAuthority.js +1 -0
- package/dist/esm/azure/financialSavingsAuthorityValidation.js +3 -0
- package/dist/esm/azure/financialSavingsCoordinateValidation.js +36 -0
- package/dist/esm/azure/financialScope.js +2 -0
- package/dist/esm/azure/financialScopeBaseline.js +12 -0
- package/dist/esm/azure/financialScopeBaselineValidation.js +15 -1
- package/dist/events/cost.d.ts +4 -0
- package/dist/events/cost.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { sha256Utf8 } from '../common/sha256.js';
|
|
|
2
2
|
import { formatExactDecimalValue, sumCanonicalDecimals } from '../common/exactDecimal.js';
|
|
3
3
|
import { toCanonicalEstimateLensV1 } from './costComposition.js';
|
|
4
4
|
import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
|
|
5
|
+
import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
|
|
5
6
|
import { isFinancialScopeBaselineEnvelopeV2 } from './financialScopeBaselineValidation.js';
|
|
6
7
|
import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
|
|
7
8
|
import { isFinancialBaselinePeriodV2 } from './financialScopeBaselineValidation.js';
|
|
@@ -141,6 +142,14 @@ export const isFinancialDataflowIdentityV1 = (value) => typeof value === 'string
|
|
|
141
142
|
export const isFinancialDataflowHashV1 = (value) => typeof value === 'string' && SHA256_ID.test(value);
|
|
142
143
|
export const isFinancialDataflowCurrencyV1 = (value) => typeof value === 'string' && CURRENCY.test(value);
|
|
143
144
|
export const isFinancialDataflowIsoInstantV1 = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
145
|
+
export const isFinancialDataflowJsonGzipArtifactDescriptorV1 = (value) => isFinancialDataflowRecordV1(value) &&
|
|
146
|
+
hasFinancialDataflowExactFieldsV1(value, ['path', 'sha256', 'byteCount', 'mediaType', 'contentEncoding']) &&
|
|
147
|
+
isFinancialDataflowIdentityV1(value.path) &&
|
|
148
|
+
isFinancialDataflowHashV1(value.sha256) &&
|
|
149
|
+
Number.isSafeInteger(value.byteCount) &&
|
|
150
|
+
Number(value.byteCount) > 0 &&
|
|
151
|
+
value.mediaType === 'application/json' &&
|
|
152
|
+
value.contentEncoding === 'gzip';
|
|
144
153
|
export const isFinancialDataflowCalendarDateV1 = (value) => {
|
|
145
154
|
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
146
155
|
return false;
|
|
@@ -310,6 +319,46 @@ export const isFinancialDataflowScopeV1 = (value) => isFinancialDataflowRecordV1
|
|
|
310
319
|
SCOPE_KINDS.has(value.kind) &&
|
|
311
320
|
isFinancialDataflowIdentityV1(value.scopeId) &&
|
|
312
321
|
isFinancialDataflowHashV1(value.scopeFingerprint);
|
|
322
|
+
export const isFinancialDataflowPeriodV1 = (value) => isFinancialDataflowRecordV1(value) &&
|
|
323
|
+
hasFinancialDataflowExactFieldsV1(value, ['windowKind', 'requested'], ['providerBillingPeriodId']) &&
|
|
324
|
+
isFinancialBaselinePeriodV2({ ...value, coverage: [], gaps: [] });
|
|
325
|
+
export const isFinancialDataflowScopeSelectorV1 = (value) => {
|
|
326
|
+
if (!isFinancialDataflowRecordV1(value) || typeof value.kind !== 'string' || !SCOPE_KINDS.has(value.kind))
|
|
327
|
+
return false;
|
|
328
|
+
if (value.kind === 'resource') {
|
|
329
|
+
return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'resourceIds']) &&
|
|
330
|
+
isFinancialDataflowSortedUniqueStringsV1(value.resourceIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers) &&
|
|
331
|
+
value.resourceIds.length === 1);
|
|
332
|
+
}
|
|
333
|
+
if (value.kind === 'resource-group') {
|
|
334
|
+
return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'resourceGroupIds']) &&
|
|
335
|
+
isFinancialDataflowSortedUniqueStringsV1(value.resourceGroupIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers) &&
|
|
336
|
+
value.resourceGroupIds.length === 1);
|
|
337
|
+
}
|
|
338
|
+
if (value.kind === 'subscription' || value.kind === 'multi-subscription') {
|
|
339
|
+
return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'subscriptionIds']) &&
|
|
340
|
+
isFinancialDataflowSortedUniqueStringsV1(value.subscriptionIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumProviderAccounts) &&
|
|
341
|
+
value.subscriptionIds.length > 0 &&
|
|
342
|
+
(value.kind !== 'subscription' || value.subscriptionIds.length === 1));
|
|
343
|
+
}
|
|
344
|
+
if (!hasFinancialDataflowExactFieldsV1(value, ['kind', 'tags', 'tagMatch']) ||
|
|
345
|
+
(value.tagMatch !== 'any' && value.tagMatch !== 'all') ||
|
|
346
|
+
!Array.isArray(value.tags) ||
|
|
347
|
+
value.tags.length === 0 ||
|
|
348
|
+
value.tags.length > 256) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const keys = value.tags.map(tag => {
|
|
352
|
+
if (!isFinancialDataflowRecordV1(tag) ||
|
|
353
|
+
!hasFinancialDataflowExactFieldsV1(tag, ['key', 'value']) ||
|
|
354
|
+
!isFinancialDataflowIdentityV1(tag.key) ||
|
|
355
|
+
!isFinancialDataflowIdentityV1(tag.value)) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
return `${tag.key}\u0000${tag.value}`;
|
|
359
|
+
});
|
|
360
|
+
return keys.every((key) => key !== undefined) && keys.every((key, index) => index === 0 || keys[index - 1] < key);
|
|
361
|
+
};
|
|
313
362
|
export const isFinancialDataflowCoordinateV1 = (value) => {
|
|
314
363
|
if (!isFinancialDataflowRecordV1(value) ||
|
|
315
364
|
!hasFinancialDataflowExactFieldsV1(value, ['companyId', 'provider', 'providerAccountRefs', 'scope', 'periodRole', 'period', 'costBasis', 'estimateLens', 'accountingCurrency'], ['requestedCurrencyCode']) ||
|
|
@@ -320,7 +369,7 @@ export const isFinancialDataflowCoordinateV1 = (value) => {
|
|
|
320
369
|
!isFinancialDataflowScopeV1(value.scope) ||
|
|
321
370
|
typeof value.periodRole !== 'string' ||
|
|
322
371
|
!PERIOD_ROLES.has(value.periodRole) ||
|
|
323
|
-
!
|
|
372
|
+
!isFinancialDataflowPeriodV1(value.period) ||
|
|
324
373
|
typeof value.costBasis !== 'string' ||
|
|
325
374
|
!COST_BASES.has(value.costBasis) ||
|
|
326
375
|
typeof value.estimateLens !== 'string' ||
|
|
@@ -411,7 +460,7 @@ const isCompositionIdentity = (value) => {
|
|
|
411
460
|
value.amount.currencyCode === currency &&
|
|
412
461
|
isReasonCodes(value.amount.reasonCodes) &&
|
|
413
462
|
value.members.some(member => member.status === 'included') &&
|
|
414
|
-
value.members.some(member => member.status === 'unavailable'));
|
|
463
|
+
(value.members.some(member => member.status === 'unavailable') || value.amount.reasonCodes.includes('coverage-incomplete')));
|
|
415
464
|
}
|
|
416
465
|
const unresolvedCurrency = value.coordinate.accountingCurrency;
|
|
417
466
|
return (value.amount.status === 'unavailable' &&
|
|
@@ -491,6 +540,9 @@ export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) =>
|
|
|
491
540
|
return false;
|
|
492
541
|
}
|
|
493
542
|
includedAmounts.push(baseline.total.amount);
|
|
543
|
+
if (baseline.status === 'available' && !isCompleteFinancialBaselinePeriodV2(baseline.period)) {
|
|
544
|
+
expectedReasonCodes.add('coverage-incomplete');
|
|
545
|
+
}
|
|
494
546
|
continue;
|
|
495
547
|
}
|
|
496
548
|
if (baseline.status !== 'unavailable' || member.reasonCode !== baseline.unavailableReason)
|
|
@@ -514,7 +566,7 @@ export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) =>
|
|
|
514
566
|
catch {
|
|
515
567
|
return false;
|
|
516
568
|
}
|
|
517
|
-
if (includedAmounts.length === composition.members.length) {
|
|
569
|
+
if (includedAmounts.length === composition.members.length && expectedReasonCodes.size === 0) {
|
|
518
570
|
return composition.amount.status === 'available' && composition.amount.amount === includedTotal;
|
|
519
571
|
}
|
|
520
572
|
return (composition.amount.status === 'partial' &&
|
|
@@ -12,6 +12,13 @@ const EVIDENCE_ROLES = new Set([
|
|
|
12
12
|
'retail-rate',
|
|
13
13
|
'commitment-quote',
|
|
14
14
|
'fx-conversion',
|
|
15
|
+
'commercial-arrangement',
|
|
16
|
+
'provider-access-observation',
|
|
17
|
+
'configuration-timeline',
|
|
18
|
+
'provider-price-schedule',
|
|
19
|
+
'pricing-function',
|
|
20
|
+
'charge-inclusion-policy',
|
|
21
|
+
'settlement-revision',
|
|
15
22
|
'eligibility-rule',
|
|
16
23
|
'recommendation-scenario-set',
|
|
17
24
|
'recommendation-lifecycle',
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export const FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1 = 'financial-policy-read-projection/v1';
|
|
2
2
|
export const FINANCIAL_POLICY_ACTION_ATTEMPT_CONTRACT_VERSION_V1 = 'financial-policy-action-attempt/v1';
|
|
3
|
+
export const FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1 = 'financial-policy-definition-command/v1';
|
|
4
|
+
export const FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1 = 'financial-policy-read-current-pointer/v1';
|
|
5
|
+
export const FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1 = 'financial-policy-action-completion/v1';
|
|
6
|
+
export const FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1 = 'financial-policy-read-manifest/v1';
|
|
@@ -1,12 +1,52 @@
|
|
|
1
1
|
import { sha256Utf8 } from '../common/sha256.js';
|
|
2
2
|
import { parseCanonicalDecimal } from '../common/exactDecimal.js';
|
|
3
3
|
import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { isFinancialAnalyticsProjectionV1 } from './financialAnalyticsValidation.js';
|
|
5
|
+
import { FINANCIAL_POLICY_ACTION_ATTEMPT_CONTRACT_VERSION_V1, FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1, FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1, } from './financialPolicyDelivery.js';
|
|
6
|
+
import { canonicalizeFinancialDataflowJsonV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowIsoInstantV1, isFinancialDataflowJsonGzipArtifactDescriptorV1, isFinancialDataflowRecordV1, } from './financialDataflowValidation.js';
|
|
7
|
+
import { createFinancialPolicyDefinitionRevisionIdV1, createFinancialPolicyEvaluationActionAuditIdV1, createFinancialPolicyEvaluationReadProjectionIdV1, isFinancialPolicyDefinitionRevisionIdentityPreimageV1, isFinancialPolicyDefinitionRevisionV1, isFinancialPolicyEvaluationV1, } from './financialPolicyValidation.js';
|
|
7
8
|
const SIGNAL_KINDS = new Set(['budget-current-spend', 'budget-forecast', 'cost-anomaly']);
|
|
8
9
|
const RESULTS = new Set(['matched', 'not-matched', 'partial', 'unavailable']);
|
|
9
10
|
const ACTION_STATUSES = new Set(['succeeded', 'retryable-failure', 'terminal-failure']);
|
|
11
|
+
const isCanonicalPolicyRevision = (value) => {
|
|
12
|
+
if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value))
|
|
13
|
+
return false;
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
return Number.isSafeInteger(parsed) && String(parsed) === value;
|
|
16
|
+
};
|
|
17
|
+
export const createFinancialPolicyDestinationsDigestV1 = (destinationRefIds) => `sha256:${sha256Utf8(canonicalizeFinancialDataflowJsonV1([...destinationRefIds].sort()))}`;
|
|
18
|
+
export const isFinancialPolicyDefinitionCommandV1 = (value) => {
|
|
19
|
+
if (!isFinancialDataflowRecordV1(value) ||
|
|
20
|
+
!hasFinancialDataflowExactFieldsV1(value, ['schemaVersion', 'contractVersion', 'displayName', 'effectiveState', 'coordinateRequest', 'criteria', 'schedule', 'destinationRefIds'], ['expectedRevision']) ||
|
|
21
|
+
value.schemaVersion !== 1 ||
|
|
22
|
+
value.contractVersion !== FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1 ||
|
|
23
|
+
(value.expectedRevision !== undefined && !isCanonicalPolicyRevision(value.expectedRevision)) ||
|
|
24
|
+
!isFinancialDataflowIdentityV1(value.displayName) ||
|
|
25
|
+
(value.effectiveState !== 'enabled' && value.effectiveState !== 'disabled') ||
|
|
26
|
+
!Array.isArray(value.destinationRefIds)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const identity = {
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
contractVersion: 'financial-policy-definition/v1',
|
|
32
|
+
companyId: 'contract-company',
|
|
33
|
+
definitionId: 'contract-definition',
|
|
34
|
+
displayName: value.displayName,
|
|
35
|
+
revision: '1',
|
|
36
|
+
effectiveState: value.effectiveState,
|
|
37
|
+
coordinateRequest: value.coordinateRequest,
|
|
38
|
+
criteria: value.criteria,
|
|
39
|
+
schedule: value.schedule,
|
|
40
|
+
destinationRefIds: value.destinationRefIds,
|
|
41
|
+
destinationsDigest: createFinancialPolicyDestinationsDigestV1(value.destinationRefIds),
|
|
42
|
+
authoredAt: '2026-01-01T00:00:00.000Z',
|
|
43
|
+
authoredByUserId: 'contract-author',
|
|
44
|
+
};
|
|
45
|
+
if (!isFinancialPolicyDefinitionRevisionIdentityPreimageV1(identity))
|
|
46
|
+
return false;
|
|
47
|
+
const definition = { ...identity, policyDefinitionRevisionId: createFinancialPolicyDefinitionRevisionIdV1(identity) };
|
|
48
|
+
return isFinancialPolicyDefinitionRevisionV1(definition);
|
|
49
|
+
};
|
|
10
50
|
const isNonNegativeDecimal = (value) => {
|
|
11
51
|
if (typeof value !== 'string' || !isCanonicalExactMoney({ amount: value, currencyCode: 'AUD' }))
|
|
12
52
|
return false;
|
|
@@ -33,6 +73,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
|
|
|
33
73
|
'companyId',
|
|
34
74
|
'definitionId',
|
|
35
75
|
'definitionRevision',
|
|
76
|
+
'policyDefinitionRevisionId',
|
|
36
77
|
'coordinateId',
|
|
37
78
|
'currentSpendCompositionId',
|
|
38
79
|
'signalKind',
|
|
@@ -40,7 +81,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
|
|
|
40
81
|
'result',
|
|
41
82
|
'reasonCode',
|
|
42
83
|
'matchedThresholds',
|
|
43
|
-
], ['analyticsProjectionId']) &&
|
|
84
|
+
], ['analyticsProjectionId', 'analyticsProjection']) &&
|
|
44
85
|
value.schemaVersion === 1 &&
|
|
45
86
|
value.contractVersion === FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1 &&
|
|
46
87
|
isFinancialDataflowHashV1(value.evaluationId) &&
|
|
@@ -48,12 +89,24 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
|
|
|
48
89
|
isFinancialDataflowIdentityV1(value.companyId) &&
|
|
49
90
|
isFinancialDataflowIdentityV1(value.definitionId) &&
|
|
50
91
|
isFinancialDataflowIdentityV1(value.definitionRevision) &&
|
|
92
|
+
isFinancialDataflowHashV1(value.policyDefinitionRevisionId) &&
|
|
51
93
|
isFinancialDataflowHashV1(value.coordinateId) &&
|
|
52
94
|
isFinancialDataflowHashV1(value.currentSpendCompositionId) &&
|
|
53
95
|
(value.analyticsProjectionId === undefined || isFinancialDataflowHashV1(value.analyticsProjectionId)) &&
|
|
96
|
+
(value.analyticsProjection === undefined || isFinancialAnalyticsProjectionV1(value.analyticsProjection)) &&
|
|
97
|
+
(value.analyticsProjectionId === undefined
|
|
98
|
+
? value.analyticsProjection === undefined
|
|
99
|
+
: value.analyticsProjection?.analyticsProjectionId === value.analyticsProjectionId &&
|
|
100
|
+
value.analyticsProjection.currentSpendCompositionId === value.currentSpendCompositionId &&
|
|
101
|
+
value.analyticsProjection.coordinate.companyId === value.companyId &&
|
|
102
|
+
value.analyticsProjection.coordinate.periodRole === 'projection-target') &&
|
|
54
103
|
typeof value.signalKind === 'string' &&
|
|
55
104
|
SIGNAL_KINDS.has(value.signalKind) &&
|
|
56
|
-
(value.signalKind === 'budget-current-spend'
|
|
105
|
+
(value.signalKind === 'budget-current-spend'
|
|
106
|
+
? value.analyticsProjectionId === undefined
|
|
107
|
+
: value.result === 'unavailable'
|
|
108
|
+
? value.analyticsProjectionId === undefined || isFinancialDataflowHashV1(value.analyticsProjectionId)
|
|
109
|
+
: value.analyticsProjectionId !== undefined) &&
|
|
57
110
|
isFinancialDataflowIsoInstantV1(value.evaluatedAt) &&
|
|
58
111
|
typeof value.result === 'string' &&
|
|
59
112
|
RESULTS.has(value.result) &&
|
|
@@ -61,7 +114,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
|
|
|
61
114
|
isMatchedThresholds(value.matchedThresholds) &&
|
|
62
115
|
Array.isArray(value.matchedThresholds) &&
|
|
63
116
|
(value.result === 'matched' ? value.matchedThresholds.length > 0 : value.matchedThresholds.length === 0);
|
|
64
|
-
export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection, evaluation) => {
|
|
117
|
+
export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection, evaluation, analyticsProjection) => {
|
|
65
118
|
if (!isFinancialPolicyEvaluationReadProjectionV1(projection) || !isFinancialPolicyEvaluationV1(evaluation))
|
|
66
119
|
return false;
|
|
67
120
|
const expected = {
|
|
@@ -72,17 +125,138 @@ export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection
|
|
|
72
125
|
companyId: evaluation.companyId,
|
|
73
126
|
definitionId: evaluation.definitionId,
|
|
74
127
|
definitionRevision: evaluation.definitionRevision,
|
|
128
|
+
policyDefinitionRevisionId: evaluation.policyDefinitionRevisionId,
|
|
75
129
|
coordinateId: evaluation.coordinateId,
|
|
76
130
|
currentSpendCompositionId: evaluation.currentSpendCompositionId,
|
|
77
131
|
...(evaluation.analyticsProjectionId === undefined ? {} : { analyticsProjectionId: evaluation.analyticsProjectionId }),
|
|
132
|
+
...(analyticsProjection === undefined ? {} : { analyticsProjection }),
|
|
78
133
|
signalKind: evaluation.signalKind,
|
|
79
134
|
evaluatedAt: evaluation.evaluatedAt,
|
|
80
135
|
result: evaluation.result,
|
|
81
136
|
reasonCode: evaluation.reasonCode,
|
|
82
137
|
matchedThresholds: evaluation.matchedThresholds,
|
|
83
138
|
};
|
|
84
|
-
return
|
|
139
|
+
return ((evaluation.analyticsProjectionId === undefined
|
|
140
|
+
? analyticsProjection === undefined
|
|
141
|
+
: analyticsProjection?.analyticsProjectionId === evaluation.analyticsProjectionId) &&
|
|
142
|
+
canonicalizeFinancialDataflowJsonV1(projection) === canonicalizeFinancialDataflowJsonV1(expected));
|
|
143
|
+
};
|
|
144
|
+
export const canonicalizeFinancialPolicyEvaluationReadCurrentPointerIdentityV1 = (value) => {
|
|
145
|
+
if (!isFinancialDataflowRecordV1(value) ||
|
|
146
|
+
!hasFinancialDataflowExactFieldsV1(value, [
|
|
147
|
+
'schemaVersion',
|
|
148
|
+
'contractVersion',
|
|
149
|
+
'companyId',
|
|
150
|
+
'definitionId',
|
|
151
|
+
'signalKind',
|
|
152
|
+
'pointerRevision',
|
|
153
|
+
'definitionRevision',
|
|
154
|
+
'policyDefinitionRevisionId',
|
|
155
|
+
'evaluationId',
|
|
156
|
+
'readProjectionId',
|
|
157
|
+
'projectionArtifactDigest',
|
|
158
|
+
'evaluatedAt',
|
|
159
|
+
'promotedAt',
|
|
160
|
+
]) ||
|
|
161
|
+
value.schemaVersion !== 1 ||
|
|
162
|
+
value.contractVersion !== FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1 ||
|
|
163
|
+
!isFinancialDataflowIdentityV1(value.companyId) ||
|
|
164
|
+
!isFinancialDataflowIdentityV1(value.definitionId) ||
|
|
165
|
+
typeof value.signalKind !== 'string' ||
|
|
166
|
+
!SIGNAL_KINDS.has(value.signalKind) ||
|
|
167
|
+
!Number.isSafeInteger(value.pointerRevision) ||
|
|
168
|
+
value.pointerRevision < 1 ||
|
|
169
|
+
!isFinancialDataflowIdentityV1(value.definitionRevision) ||
|
|
170
|
+
!isFinancialDataflowHashV1(value.policyDefinitionRevisionId) ||
|
|
171
|
+
!isFinancialDataflowHashV1(value.evaluationId) ||
|
|
172
|
+
value.readProjectionId !== createFinancialPolicyEvaluationReadProjectionIdV1(value.evaluationId) ||
|
|
173
|
+
!isFinancialDataflowHashV1(value.projectionArtifactDigest) ||
|
|
174
|
+
!isFinancialDataflowIsoInstantV1(value.evaluatedAt) ||
|
|
175
|
+
!isFinancialDataflowIsoInstantV1(value.promotedAt)) {
|
|
176
|
+
throw new TypeError('Invalid FinancialPolicyEvaluationReadCurrentPointerIdentityPreimageV1.');
|
|
177
|
+
}
|
|
178
|
+
return canonicalizeFinancialDataflowJsonV1(value);
|
|
179
|
+
};
|
|
180
|
+
export const createFinancialPolicyEvaluationReadCurrentPointerDigestV1 = (value) => `sha256:${sha256Utf8(canonicalizeFinancialPolicyEvaluationReadCurrentPointerIdentityV1(value))}`;
|
|
181
|
+
export const isFinancialPolicyEvaluationReadCurrentPointerV1 = (value) => {
|
|
182
|
+
if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'pointerDigest'))
|
|
183
|
+
return false;
|
|
184
|
+
const { pointerDigest, ...identity } = value;
|
|
185
|
+
try {
|
|
186
|
+
return (isFinancialDataflowHashV1(pointerDigest) &&
|
|
187
|
+
pointerDigest === createFinancialPolicyEvaluationReadCurrentPointerDigestV1(identity));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
export const isFinancialPolicyEvaluationReadCurrentPointerCompatibleV1 = (pointer, projection, projectionArtifactDigest) => isFinancialPolicyEvaluationReadCurrentPointerV1(pointer) &&
|
|
194
|
+
isFinancialPolicyEvaluationReadProjectionV1(projection) &&
|
|
195
|
+
isFinancialDataflowHashV1(projectionArtifactDigest) &&
|
|
196
|
+
pointer.companyId === projection.companyId &&
|
|
197
|
+
pointer.definitionId === projection.definitionId &&
|
|
198
|
+
pointer.signalKind === projection.signalKind &&
|
|
199
|
+
pointer.definitionRevision === projection.definitionRevision &&
|
|
200
|
+
pointer.policyDefinitionRevisionId === projection.policyDefinitionRevisionId &&
|
|
201
|
+
pointer.evaluationId === projection.evaluationId &&
|
|
202
|
+
pointer.readProjectionId === projection.readProjectionId &&
|
|
203
|
+
pointer.projectionArtifactDigest === projectionArtifactDigest &&
|
|
204
|
+
pointer.evaluatedAt === projection.evaluatedAt;
|
|
205
|
+
const isReadManifestIdentity = (value) => isFinancialDataflowRecordV1(value) &&
|
|
206
|
+
hasFinancialDataflowExactFieldsV1(value, [
|
|
207
|
+
'schemaVersion',
|
|
208
|
+
'contractVersion',
|
|
209
|
+
'companyId',
|
|
210
|
+
'definitionId',
|
|
211
|
+
'policyDefinitionRevisionId',
|
|
212
|
+
'definitionRevision',
|
|
213
|
+
'evaluationId',
|
|
214
|
+
'readProjectionId',
|
|
215
|
+
'signalKind',
|
|
216
|
+
'projection',
|
|
217
|
+
'publishedAt',
|
|
218
|
+
]) &&
|
|
219
|
+
value.schemaVersion === 1 &&
|
|
220
|
+
value.contractVersion === FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1 &&
|
|
221
|
+
isFinancialDataflowIdentityV1(value.companyId) &&
|
|
222
|
+
isFinancialDataflowIdentityV1(value.definitionId) &&
|
|
223
|
+
isFinancialDataflowHashV1(value.policyDefinitionRevisionId) &&
|
|
224
|
+
isFinancialDataflowIdentityV1(value.definitionRevision) &&
|
|
225
|
+
isFinancialDataflowHashV1(value.evaluationId) &&
|
|
226
|
+
value.readProjectionId === createFinancialPolicyEvaluationReadProjectionIdV1(value.evaluationId) &&
|
|
227
|
+
typeof value.signalKind === 'string' &&
|
|
228
|
+
SIGNAL_KINDS.has(value.signalKind) &&
|
|
229
|
+
isFinancialDataflowJsonGzipArtifactDescriptorV1(value.projection) &&
|
|
230
|
+
isFinancialDataflowIsoInstantV1(value.publishedAt);
|
|
231
|
+
export const createFinancialPolicyEvaluationReadManifestDigestV1 = (value) => {
|
|
232
|
+
if (!isReadManifestIdentity(value))
|
|
233
|
+
throw new TypeError('Invalid FinancialPolicyEvaluationReadManifestIdentityPreimageV1.');
|
|
234
|
+
return `sha256:${sha256Utf8(canonicalizeFinancialDataflowJsonV1(value))}`;
|
|
235
|
+
};
|
|
236
|
+
export const isFinancialPolicyEvaluationReadManifestV1 = (value) => {
|
|
237
|
+
if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'manifestDigest'))
|
|
238
|
+
return false;
|
|
239
|
+
const { manifestDigest, ...identity } = value;
|
|
240
|
+
try {
|
|
241
|
+
return (isFinancialDataflowHashV1(manifestDigest) &&
|
|
242
|
+
manifestDigest === createFinancialPolicyEvaluationReadManifestDigestV1(identity));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
85
247
|
};
|
|
248
|
+
export const isFinancialPolicyEvaluationReadManifestCompatibleV1 = (manifest, projection, verifiedProjectionArtifactDigest) => isFinancialPolicyEvaluationReadManifestV1(manifest) &&
|
|
249
|
+
isFinancialPolicyEvaluationReadProjectionV1(projection) &&
|
|
250
|
+
isFinancialDataflowHashV1(verifiedProjectionArtifactDigest) &&
|
|
251
|
+
manifest.companyId === projection.companyId &&
|
|
252
|
+
manifest.definitionId === projection.definitionId &&
|
|
253
|
+
manifest.policyDefinitionRevisionId === projection.policyDefinitionRevisionId &&
|
|
254
|
+
manifest.definitionRevision === projection.definitionRevision &&
|
|
255
|
+
manifest.evaluationId === projection.evaluationId &&
|
|
256
|
+
manifest.readProjectionId === projection.readProjectionId &&
|
|
257
|
+
manifest.signalKind === projection.signalKind &&
|
|
258
|
+
manifest.publishedAt === projection.evaluatedAt &&
|
|
259
|
+
manifest.projection.sha256 === verifiedProjectionArtifactDigest;
|
|
86
260
|
const isActionAttemptIdentity = (value) => isFinancialDataflowRecordV1(value) &&
|
|
87
261
|
hasFinancialDataflowExactFieldsV1(value, [
|
|
88
262
|
'schemaVersion',
|
|
@@ -135,3 +309,49 @@ export const isFinancialPolicyActionAttemptCompatibleV1 = (attempt, evaluation,
|
|
|
135
309
|
Date.parse(attempt.attemptedAt) >= Date.parse(evaluation.evaluatedAt) &&
|
|
136
310
|
evaluation.policyDefinitionRevisionId === definition.policyDefinitionRevisionId &&
|
|
137
311
|
definition.destinationRefIds.includes(attempt.destinationRefId);
|
|
312
|
+
export const canonicalizeFinancialPolicyActionCompletionIdentityV1 = (value) => {
|
|
313
|
+
if (!isFinancialDataflowRecordV1(value) ||
|
|
314
|
+
!hasFinancialDataflowExactFieldsV1(value, [
|
|
315
|
+
'schemaVersion',
|
|
316
|
+
'contractVersion',
|
|
317
|
+
'actionAuditId',
|
|
318
|
+
'evaluationId',
|
|
319
|
+
'companyId',
|
|
320
|
+
'destinationRefId',
|
|
321
|
+
'completedAt',
|
|
322
|
+
'executorVersion',
|
|
323
|
+
]) ||
|
|
324
|
+
value.schemaVersion !== 1 ||
|
|
325
|
+
value.contractVersion !== FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1 ||
|
|
326
|
+
!isFinancialDataflowHashV1(value.actionAuditId) ||
|
|
327
|
+
!isFinancialDataflowHashV1(value.evaluationId) ||
|
|
328
|
+
!isFinancialDataflowIdentityV1(value.companyId) ||
|
|
329
|
+
!isFinancialDataflowIdentityV1(value.destinationRefId) ||
|
|
330
|
+
!isFinancialDataflowIsoInstantV1(value.completedAt) ||
|
|
331
|
+
!isFinancialDataflowIdentityV1(value.executorVersion)) {
|
|
332
|
+
throw new TypeError('Invalid FinancialPolicyActionCompletionIdentityPreimageV1.');
|
|
333
|
+
}
|
|
334
|
+
return canonicalizeFinancialDataflowJsonV1(value);
|
|
335
|
+
};
|
|
336
|
+
export const createFinancialPolicyActionCompletionIdV1 = (value) => `sha256:${sha256Utf8(canonicalizeFinancialPolicyActionCompletionIdentityV1(value))}`;
|
|
337
|
+
export const isFinancialPolicyActionCompletionV1 = (value) => {
|
|
338
|
+
if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'completionId'))
|
|
339
|
+
return false;
|
|
340
|
+
const { completionId, ...identity } = value;
|
|
341
|
+
try {
|
|
342
|
+
return (isFinancialDataflowHashV1(completionId) &&
|
|
343
|
+
completionId === createFinancialPolicyActionCompletionIdV1(identity));
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
export const isFinancialPolicyActionCompletionCompatibleV1 = (completion, evaluation, definition) => isFinancialPolicyActionCompletionV1(completion) &&
|
|
350
|
+
isFinancialPolicyEvaluationV1(evaluation) &&
|
|
351
|
+
isFinancialPolicyDefinitionRevisionV1(definition) &&
|
|
352
|
+
completion.actionAuditId === evaluation.actionAuditId &&
|
|
353
|
+
completion.evaluationId === evaluation.evaluationId &&
|
|
354
|
+
completion.companyId === evaluation.companyId &&
|
|
355
|
+
completion.completedAt === evaluation.evaluatedAt &&
|
|
356
|
+
definition.companyId === completion.companyId &&
|
|
357
|
+
definition.destinationRefIds.includes(completion.destinationRefId);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { formatExactDecimalValue, multiplyExactDecimalValues, parseCanonicalDecimal, subtractExactDecimalValues, } from '../common/exactDecimal.js';
|
|
2
|
+
import { createFinancialDataflowCoordinateIdV1 } from './financialDataflowValidation.js';
|
|
3
|
+
import { FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1, } from './financialPolicy.js';
|
|
4
|
+
import { createFinancialPolicyEvaluationActionAuditIdV1, createFinancialPolicyEvaluationIdV1, createFinancialPolicyEvaluationReadProjectionIdV1, isFinancialPolicyEvaluationCompatibleV1, } from './financialPolicyValidation.js';
|
|
5
|
+
export const FINANCIAL_POLICY_ALGORITHM_VERSION_V1 = 'financial-policy/shared-v1';
|
|
6
|
+
const compare = (left, right) => {
|
|
7
|
+
const difference = subtractExactDecimalValues(parseCanonicalDecimal(left), parseCanonicalDecimal(right)).coefficient;
|
|
8
|
+
return difference < 0n ? -1 : difference > 0n ? 1 : 0;
|
|
9
|
+
};
|
|
10
|
+
const percentageMatched = (amount, denominator, threshold) => {
|
|
11
|
+
const parsedDenominator = parseCanonicalDecimal(denominator);
|
|
12
|
+
if (parsedDenominator.coefficient <= 0n)
|
|
13
|
+
return false;
|
|
14
|
+
const left = multiplyExactDecimalValues(parseCanonicalDecimal(amount), parseCanonicalDecimal('100'));
|
|
15
|
+
const right = multiplyExactDecimalValues(parsedDenominator, parseCanonicalDecimal(threshold));
|
|
16
|
+
return subtractExactDecimalValues(left, right).coefficient >= 0n;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Shared exact-decimal display classification for Budget surfaces. This is a
|
|
20
|
+
* read-model policy only; alert matching continues to use the thresholds in a
|
|
21
|
+
* versioned Financial Policy Definition.
|
|
22
|
+
*/
|
|
23
|
+
export const classifyFinancialBudgetPositionV1 = (request) => {
|
|
24
|
+
if (request.budgetAmount === undefined)
|
|
25
|
+
return { status: 'unbudgeted' };
|
|
26
|
+
if (request.amount === undefined)
|
|
27
|
+
return { status: 'unavailable' };
|
|
28
|
+
try {
|
|
29
|
+
const atRiskPercent = request.atRiskPercent ?? '95';
|
|
30
|
+
const amount = parseCanonicalDecimal(request.amount);
|
|
31
|
+
const budget = parseCanonicalDecimal(request.budgetAmount);
|
|
32
|
+
const risk = parseCanonicalDecimal(atRiskPercent);
|
|
33
|
+
if (budget.coefficient <= 0n || risk.coefficient < 0n || compare(atRiskPercent, '100') > 0) {
|
|
34
|
+
return { status: 'unbudgeted' };
|
|
35
|
+
}
|
|
36
|
+
const varianceAmount = formatExactDecimalValue(subtractExactDecimalValues(amount, budget));
|
|
37
|
+
const status = compare(request.amount, request.budgetAmount) > 0
|
|
38
|
+
? 'over-budget'
|
|
39
|
+
: percentageMatched(request.amount, request.budgetAmount, atRiskPercent)
|
|
40
|
+
? 'at-risk'
|
|
41
|
+
: 'on-track';
|
|
42
|
+
return {
|
|
43
|
+
status,
|
|
44
|
+
varianceAmount,
|
|
45
|
+
amount: request.amount,
|
|
46
|
+
budgetAmount: request.budgetAmount,
|
|
47
|
+
atRiskPercent,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { status: 'unavailable' };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const isExpectedCalendarMonthTargetPartial = (composition) => composition.coordinate.period.windowKind === 'calendar-month' &&
|
|
55
|
+
composition.amount.status === 'partial' &&
|
|
56
|
+
composition.amount.reasonCodes.length === 1 &&
|
|
57
|
+
composition.amount.reasonCodes[0] === 'coverage-incomplete';
|
|
58
|
+
/**
|
|
59
|
+
* Returns the exact current amount that Budget semantics may compare. An open
|
|
60
|
+
* calendar month is intentionally partial until the period closes; every
|
|
61
|
+
* other partial/unavailable state remains non-comparable.
|
|
62
|
+
*/
|
|
63
|
+
export const selectFinancialBudgetComparableCurrentAmountV1 = (composition) => composition.amount.status === 'available'
|
|
64
|
+
? composition.amount.amount
|
|
65
|
+
: isExpectedCalendarMonthTargetPartial(composition)
|
|
66
|
+
? composition.amount.knownAmount
|
|
67
|
+
: undefined;
|
|
68
|
+
const budgetThresholds = (amount, budget, thresholds) => [
|
|
69
|
+
...thresholds.amounts
|
|
70
|
+
.filter(threshold => compare(amount, threshold) >= 0)
|
|
71
|
+
.map(configuredValue => ({ thresholdKind: 'amount', configuredValue })),
|
|
72
|
+
...thresholds.percents
|
|
73
|
+
.filter(threshold => percentageMatched(amount, budget, threshold))
|
|
74
|
+
.map(configuredValue => ({ thresholdKind: 'percent', configuredValue })),
|
|
75
|
+
];
|
|
76
|
+
const anomalyThresholds = (definition, projection) => {
|
|
77
|
+
if (definition.criteria.kind !== 'cost-anomaly' || projection.status === 'unavailable' || projection.result.kind !== 'anomaly')
|
|
78
|
+
return [];
|
|
79
|
+
const configured = [
|
|
80
|
+
...(definition.criteria.minimumAmount === undefined
|
|
81
|
+
? []
|
|
82
|
+
: [{ thresholdKind: 'minimum-amount', configuredValue: definition.criteria.minimumAmount }]),
|
|
83
|
+
...(definition.criteria.minimumDelta === undefined
|
|
84
|
+
? []
|
|
85
|
+
: [{ thresholdKind: 'minimum-delta', configuredValue: definition.criteria.minimumDelta }]),
|
|
86
|
+
...(definition.criteria.minimumPercentChange === undefined
|
|
87
|
+
? []
|
|
88
|
+
: [{ thresholdKind: 'minimum-percent-change', configuredValue: definition.criteria.minimumPercentChange }]),
|
|
89
|
+
];
|
|
90
|
+
return projection.result.events.some(event => configured.every(threshold => {
|
|
91
|
+
if (threshold.thresholdKind === 'minimum-amount')
|
|
92
|
+
return compare(event.observed.amount, threshold.configuredValue) >= 0;
|
|
93
|
+
if (threshold.thresholdKind === 'minimum-delta')
|
|
94
|
+
return compare(event.delta.amount, threshold.configuredValue) >= 0;
|
|
95
|
+
return percentageMatched(event.delta.amount, event.expected.amount, threshold.configuredValue);
|
|
96
|
+
}))
|
|
97
|
+
? configured
|
|
98
|
+
: [];
|
|
99
|
+
};
|
|
100
|
+
const materialize = (identity, definition, currentSpend, analytics) => {
|
|
101
|
+
const evaluationId = createFinancialPolicyEvaluationIdV1(identity);
|
|
102
|
+
const evaluation = {
|
|
103
|
+
...identity,
|
|
104
|
+
evaluationId,
|
|
105
|
+
readProjectionId: createFinancialPolicyEvaluationReadProjectionIdV1(evaluationId),
|
|
106
|
+
actionAuditId: createFinancialPolicyEvaluationActionAuditIdV1(evaluationId),
|
|
107
|
+
};
|
|
108
|
+
if (!isFinancialPolicyEvaluationCompatibleV1(evaluation, definition, currentSpend, analytics)) {
|
|
109
|
+
throw new TypeError('Financial policy evaluation does not bind its definition and exact financial inputs.');
|
|
110
|
+
}
|
|
111
|
+
return evaluation;
|
|
112
|
+
};
|
|
113
|
+
export const evaluateFinancialPolicyV1 = (request) => {
|
|
114
|
+
if (request.definition.effectiveState !== 'enabled')
|
|
115
|
+
return [];
|
|
116
|
+
const common = {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
contractVersion: FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1,
|
|
119
|
+
companyId: request.definition.companyId,
|
|
120
|
+
policyDefinitionRevisionId: request.definition.policyDefinitionRevisionId,
|
|
121
|
+
definitionId: request.definition.definitionId,
|
|
122
|
+
definitionRevision: request.definition.revision,
|
|
123
|
+
coordinateId: createFinancialDataflowCoordinateIdV1(request.currentSpend.coordinate),
|
|
124
|
+
currentSpendCompositionId: request.currentSpend.compositionId,
|
|
125
|
+
evaluatedAt: request.evaluatedAt,
|
|
126
|
+
policyAlgorithmVersion: FINANCIAL_POLICY_ALGORITHM_VERSION_V1,
|
|
127
|
+
};
|
|
128
|
+
if (request.definition.criteria.kind === 'budget') {
|
|
129
|
+
const evaluations = [];
|
|
130
|
+
const currentThresholds = request.definition.criteria.currentSpendThresholds;
|
|
131
|
+
if (currentThresholds) {
|
|
132
|
+
const expectedOpenCalendarMonth = isExpectedCalendarMonthTargetPartial(request.currentSpend);
|
|
133
|
+
const comparableCurrentAmount = selectFinancialBudgetComparableCurrentAmountV1(request.currentSpend);
|
|
134
|
+
const matched = comparableCurrentAmount === undefined
|
|
135
|
+
? []
|
|
136
|
+
: budgetThresholds(comparableCurrentAmount, request.definition.criteria.budget.amount, currentThresholds);
|
|
137
|
+
const unavailable = request.currentSpend.amount.status === 'unavailable';
|
|
138
|
+
const partial = request.currentSpend.amount.status === 'partial' && !expectedOpenCalendarMonth;
|
|
139
|
+
evaluations.push(materialize({
|
|
140
|
+
...common,
|
|
141
|
+
signalKind: 'budget-current-spend',
|
|
142
|
+
result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
|
|
143
|
+
reasonCode: unavailable
|
|
144
|
+
? 'current-spend-unavailable'
|
|
145
|
+
: partial
|
|
146
|
+
? 'current-spend-partial'
|
|
147
|
+
: expectedOpenCalendarMonth && matched.length > 0
|
|
148
|
+
? 'current-spend-threshold-matched-open-period'
|
|
149
|
+
: expectedOpenCalendarMonth
|
|
150
|
+
? 'current-spend-threshold-not-matched-open-period'
|
|
151
|
+
: matched.length > 0
|
|
152
|
+
? 'current-spend-threshold-matched'
|
|
153
|
+
: 'current-spend-threshold-not-matched',
|
|
154
|
+
matchedThresholds: matched,
|
|
155
|
+
}, request.definition, request.currentSpend));
|
|
156
|
+
}
|
|
157
|
+
const forecastThresholds = request.definition.criteria.forecastThresholds;
|
|
158
|
+
if (forecastThresholds) {
|
|
159
|
+
const projection = request.analytics?.forecast;
|
|
160
|
+
if (!projection) {
|
|
161
|
+
evaluations.push(materialize({
|
|
162
|
+
...common,
|
|
163
|
+
signalKind: 'budget-forecast',
|
|
164
|
+
result: 'unavailable',
|
|
165
|
+
reasonCode: 'analytics-projection-unavailable',
|
|
166
|
+
matchedThresholds: [],
|
|
167
|
+
}, request.definition, request.currentSpend));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const unavailable = projection.status === 'unavailable' || request.currentSpend.amount.status === 'unavailable';
|
|
171
|
+
const partial = projection.status === 'partial' ||
|
|
172
|
+
(request.currentSpend.amount.status === 'partial' && !isExpectedCalendarMonthTargetPartial(request.currentSpend));
|
|
173
|
+
const matched = !unavailable && !partial && projection.result.kind === 'forecast'
|
|
174
|
+
? budgetThresholds(projection.result.projectedTotal.amount, request.definition.criteria.budget.amount, forecastThresholds)
|
|
175
|
+
: [];
|
|
176
|
+
evaluations.push(materialize({
|
|
177
|
+
...common,
|
|
178
|
+
analyticsProjectionId: projection.analyticsProjectionId,
|
|
179
|
+
signalKind: 'budget-forecast',
|
|
180
|
+
result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
|
|
181
|
+
reasonCode: unavailable
|
|
182
|
+
? 'forecast-unavailable'
|
|
183
|
+
: partial
|
|
184
|
+
? 'forecast-partial'
|
|
185
|
+
: matched.length > 0
|
|
186
|
+
? 'forecast-threshold-matched'
|
|
187
|
+
: 'forecast-threshold-not-matched',
|
|
188
|
+
matchedThresholds: matched,
|
|
189
|
+
}, request.definition, request.currentSpend, projection));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return evaluations;
|
|
193
|
+
}
|
|
194
|
+
const projection = request.analytics?.anomaly;
|
|
195
|
+
if (!projection) {
|
|
196
|
+
return [
|
|
197
|
+
materialize({
|
|
198
|
+
...common,
|
|
199
|
+
signalKind: 'cost-anomaly',
|
|
200
|
+
result: 'unavailable',
|
|
201
|
+
reasonCode: 'analytics-projection-unavailable',
|
|
202
|
+
matchedThresholds: [],
|
|
203
|
+
}, request.definition, request.currentSpend),
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
const unavailable = projection.status === 'unavailable' || request.currentSpend.amount.status === 'unavailable';
|
|
207
|
+
const partial = projection.status === 'partial' || request.currentSpend.amount.status === 'partial';
|
|
208
|
+
const matched = unavailable || partial ? [] : anomalyThresholds(request.definition, projection);
|
|
209
|
+
return [
|
|
210
|
+
materialize({
|
|
211
|
+
...common,
|
|
212
|
+
analyticsProjectionId: projection.analyticsProjectionId,
|
|
213
|
+
signalKind: 'cost-anomaly',
|
|
214
|
+
result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
|
|
215
|
+
reasonCode: unavailable
|
|
216
|
+
? 'anomaly-unavailable'
|
|
217
|
+
: partial
|
|
218
|
+
? 'anomaly-partial'
|
|
219
|
+
: matched.length > 0
|
|
220
|
+
? 'anomaly-threshold-matched'
|
|
221
|
+
: 'anomaly-threshold-not-matched',
|
|
222
|
+
matchedThresholds: matched,
|
|
223
|
+
}, request.definition, request.currentSpend, projection),
|
|
224
|
+
];
|
|
225
|
+
};
|