@spottoai/types-package 1.0.2-dev1138.2 → 1.0.2-dev1138.5
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/README.md +9 -0
- package/dist/accounts/accounts.d.ts +77 -4
- package/dist/accounts/accounts.d.ts.map +1 -1
- package/dist/accounts/accounts.js +2 -1
- package/dist/accounts/accounts.js.map +1 -1
- package/dist/accounts/azureManualOnboarding.d.ts +114 -0
- package/dist/accounts/azureManualOnboarding.d.ts.map +1 -0
- package/dist/accounts/azureManualOnboarding.js +12 -0
- package/dist/accounts/azureManualOnboarding.js.map +1 -0
- package/dist/accounts/azureSpSetup.d.ts +128 -31
- package/dist/accounts/azureSpSetup.d.ts.map +1 -1
- package/dist/accounts/azureSpSetup.js +15 -0
- package/dist/accounts/azureSpSetup.js.map +1 -1
- package/dist/accounts/azureSpSetupDurable.d.ts +8 -0
- package/dist/accounts/azureSpSetupDurable.d.ts.map +1 -1
- package/dist/accounts/azureSpSetupDurable.js +28 -0
- package/dist/accounts/azureSpSetupDurable.js.map +1 -1
- package/dist/accounts/index.d.ts +1 -0
- package/dist/accounts/index.d.ts.map +1 -1
- package/dist/accounts/index.js +1 -0
- package/dist/accounts/index.js.map +1 -1
- package/dist/accounts/readPermissions.d.ts +1 -1
- package/dist/accounts/readPermissions.d.ts.map +1 -1
- package/dist/accounts/readPermissions.js.map +1 -1
- package/dist/accounts/validation.d.ts +24 -0
- package/dist/accounts/validation.d.ts.map +1 -1
- package/dist/ai/index.d.ts +14 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +12 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/aws/commitments-planning.d.ts +5 -0
- package/dist/aws/commitments-planning.d.ts.map +1 -1
- package/dist/aws/commitments-planning.js +19 -0
- package/dist/aws/commitments-planning.js.map +1 -1
- package/dist/aws/estates.d.ts +1 -1
- package/dist/aws/estates.d.ts.map +1 -1
- package/dist/aws/estates.js +7 -1
- package/dist/aws/estates.js.map +1 -1
- package/dist/aws/index.d.ts +5 -0
- package/dist/aws/index.d.ts.map +1 -1
- package/dist/aws/index.js +5 -0
- package/dist/aws/index.js.map +1 -1
- package/dist/aws/onboarding.d.ts +3 -0
- package/dist/aws/onboarding.d.ts.map +1 -1
- package/dist/aws/organizationCommitments.d.ts +260 -0
- package/dist/aws/organizationCommitments.d.ts.map +1 -0
- package/dist/aws/organizationCommitments.js +53 -0
- package/dist/aws/organizationCommitments.js.map +1 -0
- package/dist/aws/organizationCommitmentsApiValidation.d.ts +21 -0
- package/dist/aws/organizationCommitmentsApiValidation.d.ts.map +1 -0
- package/dist/aws/organizationCommitmentsApiValidation.js +226 -0
- package/dist/aws/organizationCommitmentsApiValidation.js.map +1 -0
- package/dist/aws/organizationCommitmentsValidation.d.ts +12 -0
- package/dist/aws/organizationCommitmentsValidation.d.ts.map +1 -0
- package/dist/aws/organizationCommitmentsValidation.js +228 -0
- package/dist/aws/organizationCommitmentsValidation.js.map +1 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.d.ts +9 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.d.ts.map +1 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js +407 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js.map +1 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.d.ts +17 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.d.ts.map +1 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js +6 -0
- package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js.map +1 -0
- package/dist/aws/requests.d.ts +13 -2
- package/dist/aws/requests.d.ts.map +1 -1
- package/dist/aws/requests.js +1 -1
- package/dist/aws/requests.js.map +1 -1
- package/dist/azure/commitmentsPlanning.d.ts +2 -2
- package/dist/azure/commitmentsPlanning.d.ts.map +1 -1
- package/dist/azure/financialAnalytics.d.ts +68 -2
- package/dist/azure/financialAnalytics.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDelivery.d.ts +2 -0
- 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/financialAnalyticsDisplayKernel.d.ts +12 -0
- package/dist/azure/financialAnalyticsDisplayKernel.d.ts.map +1 -0
- package/dist/azure/financialAnalyticsDisplayKernel.js +144 -0
- package/dist/azure/financialAnalyticsDisplayKernel.js.map +1 -0
- package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsValidation.js +61 -7
- package/dist/azure/financialAnalyticsValidation.js.map +1 -1
- package/dist/azure/financialAuthorityProjectionKernel.d.ts +7 -0
- package/dist/azure/financialAuthorityProjectionKernel.d.ts.map +1 -0
- package/dist/azure/financialAuthorityProjectionKernel.js +165 -0
- package/dist/azure/financialAuthorityProjectionKernel.js.map +1 -0
- package/dist/azure/financialAuthorityView.d.ts +4 -0
- package/dist/azure/financialAuthorityView.d.ts.map +1 -1
- package/dist/azure/financialAuthorityView.js.map +1 -1
- package/dist/azure/financialAuthorityViewValidation.d.ts +3 -1
- package/dist/azure/financialAuthorityViewValidation.d.ts.map +1 -1
- package/dist/azure/financialAuthorityViewValidation.js +206 -2
- package/dist/azure/financialAuthorityViewValidation.js.map +1 -1
- package/dist/azure/financialChargeComposition.d.ts +81 -0
- package/dist/azure/financialChargeComposition.d.ts.map +1 -0
- package/dist/azure/financialChargeComposition.js +41 -0
- package/dist/azure/financialChargeComposition.js.map +1 -0
- package/dist/azure/financialChargeCompositionKernel.d.ts +3 -0
- package/dist/azure/financialChargeCompositionKernel.d.ts.map +1 -0
- package/dist/azure/financialChargeCompositionKernel.js +53 -0
- package/dist/azure/financialChargeCompositionKernel.js.map +1 -0
- package/dist/azure/financialChargeCompositionValidation.d.ts +8 -0
- package/dist/azure/financialChargeCompositionValidation.d.ts.map +1 -0
- package/dist/azure/financialChargeCompositionValidation.js +164 -0
- package/dist/azure/financialChargeCompositionValidation.js.map +1 -0
- package/dist/azure/financialDataflow.d.ts +8 -1
- package/dist/azure/financialDataflow.d.ts.map +1 -1
- package/dist/azure/financialDataflow.js.map +1 -1
- package/dist/azure/financialDataflowKernel.d.ts +2 -0
- package/dist/azure/financialDataflowKernel.d.ts.map +1 -1
- package/dist/azure/financialDataflowKernel.js +60 -7
- package/dist/azure/financialDataflowKernel.js.map +1 -1
- package/dist/azure/financialDataflowValidation.d.ts +1 -1
- package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
- package/dist/azure/financialDataflowValidation.js +146 -28
- package/dist/azure/financialDataflowValidation.js.map +1 -1
- package/dist/azure/financialPolicy.d.ts +3 -0
- package/dist/azure/financialPolicy.d.ts.map +1 -1
- package/dist/azure/financialPolicy.js.map +1 -1
- package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
- package/dist/azure/financialPolicyValidation.js +5 -0
- package/dist/azure/financialPolicyValidation.js.map +1 -1
- package/dist/azure/financialProjection.d.ts +11 -1
- package/dist/azure/financialProjection.d.ts.map +1 -1
- package/dist/azure/financialProjection.js.map +1 -1
- package/dist/azure/financialProjectionValidation.d.ts.map +1 -1
- package/dist/azure/financialProjectionValidation.js +43 -3
- package/dist/azure/financialProjectionValidation.js.map +1 -1
- package/dist/azure/financialSavingsAuthority.d.ts +25 -4
- package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.d.ts +6 -2
- package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.js +126 -2
- package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.js +62 -19
- package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
- package/dist/azure/financialSavingsSurfaceProjection.d.ts +58 -3
- package/dist/azure/financialSavingsSurfaceProjection.d.ts.map +1 -1
- package/dist/azure/financialSavingsSurfaceProjection.js +3 -1
- package/dist/azure/financialSavingsSurfaceProjection.js.map +1 -1
- package/dist/azure/financialSavingsSurfaceProjectionKernel.d.ts +14 -0
- package/dist/azure/financialSavingsSurfaceProjectionKernel.d.ts.map +1 -0
- package/dist/azure/financialSavingsSurfaceProjectionKernel.js +326 -0
- package/dist/azure/financialSavingsSurfaceProjectionKernel.js.map +1 -0
- package/dist/azure/financialSavingsSurfaceProjectionValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsSurfaceProjectionValidation.js +126 -12
- package/dist/azure/financialSavingsSurfaceProjectionValidation.js.map +1 -1
- package/dist/azure/financialScope.d.ts +6 -0
- package/dist/azure/financialScope.d.ts.map +1 -1
- package/dist/azure/financialScope.js +6 -0
- package/dist/azure/financialScope.js.map +1 -1
- package/dist/azure/governance.d.ts +145 -0
- package/dist/azure/governance.d.ts.map +1 -1
- package/dist/azure/governance.js +2 -1
- package/dist/azure/governance.js.map +1 -1
- package/dist/azure/metrics.d.ts +2 -2
- package/dist/azure/metrics.d.ts.map +1 -1
- package/dist/azure/payloads.d.ts +19 -0
- package/dist/azure/payloads.d.ts.map +1 -1
- package/dist/azure/prices.d.ts +16 -3
- package/dist/azure/prices.d.ts.map +1 -1
- package/dist/azure/recommendationState.d.ts +1 -1
- package/dist/azure/recommendationState.d.ts.map +1 -1
- package/dist/azure/recommendations.d.ts +15 -5
- package/dist/azure/recommendations.d.ts.map +1 -1
- package/dist/azure/reports.d.ts +3 -2
- package/dist/azure/reports.d.ts.map +1 -1
- package/dist/azure/reports.js.map +1 -1
- package/dist/azure/resources.d.ts +12 -0
- package/dist/azure/resources.d.ts.map +1 -1
- package/dist/azure/serviceRetirement.d.ts +33 -2
- package/dist/azure/serviceRetirement.d.ts.map +1 -1
- package/dist/azure/subscriptions.d.ts +13 -2
- package/dist/azure/subscriptions.d.ts.map +1 -1
- package/dist/azure/views.d.ts +26 -3
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js.map +1 -1
- package/dist/azure/vmPatchManagement.d.ts +109 -0
- package/dist/azure/vmPatchManagement.d.ts.map +1 -0
- package/dist/azure/vmPatchManagement.js +7 -0
- package/dist/azure/vmPatchManagement.js.map +1 -0
- package/dist/common/provider.d.ts +53 -0
- package/dist/common/provider.d.ts.map +1 -1
- package/dist/common/provider.js.map +1 -1
- package/dist/environment/canonicalization.d.ts +12 -0
- package/dist/environment/canonicalization.d.ts.map +1 -0
- package/dist/environment/canonicalization.js +40 -0
- package/dist/environment/canonicalization.js.map +1 -0
- package/dist/environment/contracts.d.ts +196 -0
- package/dist/environment/contracts.d.ts.map +1 -0
- package/dist/environment/contracts.js +21 -0
- package/dist/environment/contracts.js.map +1 -0
- package/dist/environment/index.d.ts +5 -0
- package/dist/environment/index.d.ts.map +1 -0
- package/dist/environment/index.js +21 -0
- package/dist/environment/index.js.map +1 -0
- package/dist/environment/internal.d.ts +21 -0
- package/dist/environment/internal.d.ts.map +1 -0
- package/dist/environment/internal.js +113 -0
- package/dist/environment/internal.js.map +1 -0
- package/dist/environment/references.d.ts +18 -0
- package/dist/environment/references.d.ts.map +1 -0
- package/dist/environment/references.js +206 -0
- package/dist/environment/references.js.map +1 -0
- package/dist/environment/validation.d.ts +28 -0
- package/dist/environment/validation.d.ts.map +1 -0
- package/dist/environment/validation.js +320 -0
- package/dist/environment/validation.js.map +1 -0
- package/dist/esm/accounts/accounts.js +1 -0
- package/dist/esm/accounts/azureManualOnboarding.js +8 -0
- package/dist/esm/accounts/azureSpSetup.js +14 -1
- package/dist/esm/accounts/azureSpSetupDurable.js +27 -1
- package/dist/esm/accounts/index.js +1 -0
- package/dist/esm/ai/index.js +10 -1
- package/dist/esm/aws/commitments-planning.js +5 -0
- package/dist/esm/aws/estates.js +7 -1
- package/dist/esm/aws/index.js +5 -0
- package/dist/esm/aws/organizationCommitments.js +48 -0
- package/dist/esm/aws/organizationCommitmentsApiValidation.js +220 -0
- package/dist/esm/aws/organizationCommitmentsValidation.js +224 -0
- package/dist/esm/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js +402 -0
- package/dist/esm/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js +2 -0
- package/dist/esm/aws/requests.js +1 -1
- package/dist/esm/azure/financialAnalyticsDelivery.js +2 -0
- package/dist/esm/azure/financialAnalyticsDisplayKernel.js +139 -0
- package/dist/esm/azure/financialAnalyticsValidation.js +61 -7
- package/dist/esm/azure/financialAuthorityProjectionKernel.js +159 -0
- package/dist/esm/azure/financialAuthorityViewValidation.js +204 -1
- package/dist/esm/azure/financialChargeComposition.js +36 -0
- package/dist/esm/azure/financialChargeCompositionKernel.js +48 -0
- package/dist/esm/azure/financialChargeCompositionValidation.js +155 -0
- package/dist/esm/azure/financialDataflowKernel.js +60 -7
- package/dist/esm/azure/financialDataflowValidation.js +146 -28
- package/dist/esm/azure/financialPolicyValidation.js +5 -0
- package/dist/esm/azure/financialProjectionValidation.js +43 -3
- package/dist/esm/azure/financialSavingsAuthorityValidation.js +124 -2
- package/dist/esm/azure/financialSavingsCoordinateValidation.js +62 -19
- package/dist/esm/azure/financialSavingsSurfaceProjection.js +2 -0
- package/dist/esm/azure/financialSavingsSurfaceProjectionKernel.js +318 -0
- package/dist/esm/azure/financialSavingsSurfaceProjectionValidation.js +126 -12
- package/dist/esm/azure/financialScope.js +6 -0
- package/dist/esm/azure/governance.js +1 -0
- package/dist/esm/azure/vmPatchManagement.js +3 -0
- package/dist/esm/entries/commitments-planning.js +2 -2
- package/dist/esm/entries/environment.js +3 -0
- package/dist/esm/environment/canonicalization.js +34 -0
- package/dist/esm/environment/contracts.js +17 -0
- package/dist/esm/environment/index.js +4 -0
- package/dist/esm/environment/internal.js +97 -0
- package/dist/esm/environment/references.js +194 -0
- package/dist/esm/environment/validation.js +303 -0
- package/dist/esm/index.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/portfolio/portfolioOperations.d.ts +1 -1
- package/dist/portfolio/portfolioOperations.d.ts.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { formatExactDecimalValue, sumCanonicalDecimals } from '../common/exactDecimal.js';
|
|
2
|
+
import { sha256Utf8 } from '../common/sha256.js';
|
|
3
|
+
import { FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1, resolveFinancialChargeInclusionPolicyV1, } from './financialChargeComposition.js';
|
|
4
|
+
import { canonicalizeFinancialDataflowJsonV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowCurrencyV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowRecordV1, isFinancialDataflowValueWithinLimitsV1, } from './financialDataflowValidation.js';
|
|
5
|
+
import { isFinancialBaselinePeriodV2 } from './financialScopeBaselineValidation.js';
|
|
6
|
+
import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
|
|
7
|
+
const SOURCES = new Set(['azure-native', 'marketplace', 'unknown']);
|
|
8
|
+
const RECURRENCES = new Set(['one-time', 'recurring', 'usage-based', 'unknown']);
|
|
9
|
+
const CLASSIFICATIONS = new Set(['usage', 'purchase', 'adjustment', 'tax', 'credit', 'refund', 'residual']);
|
|
10
|
+
const COST_BASES = new Set(['billed', 'amortized']);
|
|
11
|
+
const ESTIMATE_LENSES = new Set(['actual-only', 'actual-plus-estimated', 'estimates-only']);
|
|
12
|
+
const isSortedUniqueHashes = (value) => Array.isArray(value) && value.length > 0 && value.length <= 20000 && value.every(isFinancialDataflowHashV1) &&
|
|
13
|
+
value.every((entry, index) => index === 0 || value[index - 1] < entry);
|
|
14
|
+
const isBucket = (value) => isFinancialDataflowRecordV1(value) &&
|
|
15
|
+
hasFinancialDataflowExactFieldsV1(value, [
|
|
16
|
+
'chargeSource',
|
|
17
|
+
'chargeRecurrence',
|
|
18
|
+
'chargeClassification',
|
|
19
|
+
'amount',
|
|
20
|
+
'componentIds',
|
|
21
|
+
'evidenceRefIds',
|
|
22
|
+
]) &&
|
|
23
|
+
SOURCES.has(value.chargeSource) &&
|
|
24
|
+
RECURRENCES.has(value.chargeRecurrence) &&
|
|
25
|
+
CLASSIFICATIONS.has(value.chargeClassification) &&
|
|
26
|
+
isCanonicalExactMoney({ amount: value.amount, currencyCode: 'AUD' }) &&
|
|
27
|
+
isSortedUniqueHashes(value.componentIds) &&
|
|
28
|
+
isSortedUniqueHashes(value.evidenceRefIds);
|
|
29
|
+
const isIdentity = (value) => {
|
|
30
|
+
if (!isFinancialDataflowRecordV1(value) ||
|
|
31
|
+
!hasFinancialDataflowExactFieldsV1(value, [
|
|
32
|
+
'schemaVersion',
|
|
33
|
+
'contractVersion',
|
|
34
|
+
'baselineId',
|
|
35
|
+
'ownerScopeId',
|
|
36
|
+
'period',
|
|
37
|
+
'costBasis',
|
|
38
|
+
'estimateLens',
|
|
39
|
+
'accountingCurrencyCode',
|
|
40
|
+
'buckets',
|
|
41
|
+
'reconciliation',
|
|
42
|
+
'algorithmVersion',
|
|
43
|
+
]) ||
|
|
44
|
+
value.schemaVersion !== FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1 ||
|
|
45
|
+
value.contractVersion !== FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1 ||
|
|
46
|
+
!isFinancialDataflowHashV1(value.baselineId) ||
|
|
47
|
+
!isFinancialDataflowIdentityV1(value.ownerScopeId) ||
|
|
48
|
+
!isFinancialBaselinePeriodV2(value.period) ||
|
|
49
|
+
!COST_BASES.has(value.costBasis) ||
|
|
50
|
+
!ESTIMATE_LENSES.has(value.estimateLens) ||
|
|
51
|
+
!isFinancialDataflowCurrencyV1(value.accountingCurrencyCode) ||
|
|
52
|
+
!Array.isArray(value.buckets) ||
|
|
53
|
+
value.buckets.length === 0 ||
|
|
54
|
+
value.buckets.length > 20000 ||
|
|
55
|
+
!value.buckets.every(isBucket) ||
|
|
56
|
+
!isFinancialDataflowIdentityV1(value.algorithmVersion) ||
|
|
57
|
+
!isFinancialDataflowRecordV1(value.reconciliation) ||
|
|
58
|
+
!hasFinancialDataflowExactFieldsV1(value.reconciliation, ['status', 'bucketTotal', 'sourceTotal', 'difference']) ||
|
|
59
|
+
value.reconciliation.status !== 'reconciled' ||
|
|
60
|
+
value.reconciliation.difference !== '0' ||
|
|
61
|
+
!isCanonicalExactMoney({ amount: value.reconciliation.bucketTotal, currencyCode: value.accountingCurrencyCode }) ||
|
|
62
|
+
!isCanonicalExactMoney({ amount: value.reconciliation.sourceTotal, currencyCode: value.accountingCurrencyCode }))
|
|
63
|
+
return false;
|
|
64
|
+
const componentIds = value.buckets.flatMap(bucket => bucket.componentIds);
|
|
65
|
+
if (new Set(componentIds).size !== componentIds.length)
|
|
66
|
+
return false;
|
|
67
|
+
const keys = value.buckets.map(bucket => `${bucket.chargeSource}\u0000${bucket.chargeRecurrence}\u0000${bucket.chargeClassification}`);
|
|
68
|
+
if (!keys.every((key, index) => index === 0 || keys[index - 1] < key))
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const bucketTotal = formatExactDecimalValue(sumCanonicalDecimals(value.buckets.map(bucket => bucket.amount)));
|
|
72
|
+
return bucketTotal === value.reconciliation.bucketTotal && bucketTotal === value.reconciliation.sourceTotal;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
export const canonicalizeFinancialChargeCompositionIdentityV1 = (value) => {
|
|
79
|
+
if (!isFinancialDataflowValueWithinLimitsV1(value) || !isIdentity(value)) {
|
|
80
|
+
throw new TypeError('Invalid FinancialChargeCompositionIdentityPreimageV1.');
|
|
81
|
+
}
|
|
82
|
+
return canonicalizeFinancialDataflowJsonV1(value);
|
|
83
|
+
};
|
|
84
|
+
export const createFinancialChargeCompositionIdV1 = (value) => `sha256:${sha256Utf8(canonicalizeFinancialChargeCompositionIdentityV1(value))}`;
|
|
85
|
+
export const isFinancialChargeCompositionV1 = (value) => {
|
|
86
|
+
if (!isFinancialDataflowValueWithinLimitsV1(value) ||
|
|
87
|
+
!isFinancialDataflowRecordV1(value) ||
|
|
88
|
+
!hasFinancialDataflowExactFieldsV1(value, [
|
|
89
|
+
'chargeCompositionId',
|
|
90
|
+
'schemaVersion',
|
|
91
|
+
'contractVersion',
|
|
92
|
+
'baselineId',
|
|
93
|
+
'ownerScopeId',
|
|
94
|
+
'period',
|
|
95
|
+
'costBasis',
|
|
96
|
+
'estimateLens',
|
|
97
|
+
'accountingCurrencyCode',
|
|
98
|
+
'buckets',
|
|
99
|
+
'reconciliation',
|
|
100
|
+
'algorithmVersion',
|
|
101
|
+
]))
|
|
102
|
+
return false;
|
|
103
|
+
const { chargeCompositionId, ...identity } = value;
|
|
104
|
+
return isFinancialDataflowHashV1(chargeCompositionId) && isIdentity(identity) &&
|
|
105
|
+
chargeCompositionId === createFinancialChargeCompositionIdV1(identity);
|
|
106
|
+
};
|
|
107
|
+
export const isFinancialChargeInclusionPolicyRefV1 = (value) => isFinancialDataflowRecordV1(value) &&
|
|
108
|
+
hasFinancialDataflowExactFieldsV1(value, ['policyId', 'policyDigest']) &&
|
|
109
|
+
isFinancialDataflowIdentityV1(value.policyId) &&
|
|
110
|
+
isFinancialDataflowHashV1(value.policyDigest) &&
|
|
111
|
+
resolveFinancialChargeInclusionPolicyV1(value) !== undefined;
|
|
112
|
+
const sumBuckets = (buckets) => formatExactDecimalValue(sumCanonicalDecimals(buckets.map(bucket => bucket.amount)));
|
|
113
|
+
const sourcesFor = (policy, role) => role === 'include' ? policy.includeSources : role === 'exclude' ? policy.excludeSources : policy.withholdSources;
|
|
114
|
+
export const selectFinancialChargesV1 = (composition, policyRef) => {
|
|
115
|
+
if (!isFinancialChargeCompositionV1(composition))
|
|
116
|
+
throw new TypeError('Invalid financial charge composition.');
|
|
117
|
+
const policy = resolveFinancialChargeInclusionPolicyV1(policyRef);
|
|
118
|
+
if (!policy)
|
|
119
|
+
throw new TypeError('Unregistered financial charge-inclusion policy.');
|
|
120
|
+
const partition = (role) => composition.buckets.filter(bucket => sourcesFor(policy, role).includes(bucket.chargeSource));
|
|
121
|
+
const included = partition('include');
|
|
122
|
+
const excluded = partition('exclude');
|
|
123
|
+
const withheld = partition('withhold');
|
|
124
|
+
if (included.length + excluded.length + withheld.length !== composition.buckets.length) {
|
|
125
|
+
throw new TypeError('Financial charge-inclusion policy does not partition every charge source.');
|
|
126
|
+
}
|
|
127
|
+
const includedAmount = sumBuckets(included);
|
|
128
|
+
const excludedAmount = sumBuckets(excluded);
|
|
129
|
+
const withheldAmount = sumBuckets(withheld);
|
|
130
|
+
const forecastEligibleAmount = sumBuckets(included.filter(bucket => bucket.chargeRecurrence === 'recurring' || bucket.chargeRecurrence === 'usage-based'));
|
|
131
|
+
const oneTimeAmount = sumBuckets(included.filter(bucket => bucket.chargeRecurrence === 'one-time'));
|
|
132
|
+
const unknownRecurrenceAmount = sumBuckets(included.filter(bucket => bucket.chargeRecurrence === 'unknown'));
|
|
133
|
+
const reasonCodes = [];
|
|
134
|
+
if (withheldAmount !== '0')
|
|
135
|
+
reasonCodes.push('charge-source-unknown');
|
|
136
|
+
const forecastReasonCodes = [...reasonCodes];
|
|
137
|
+
if (unknownRecurrenceAmount !== '0')
|
|
138
|
+
forecastReasonCodes.push('charge-recurrence-unknown');
|
|
139
|
+
const result = {
|
|
140
|
+
status: withheldAmount === '0' ? 'available' : 'partial',
|
|
141
|
+
includedAmount,
|
|
142
|
+
excludedAmount,
|
|
143
|
+
withheldAmount,
|
|
144
|
+
forecastEligibleAmount,
|
|
145
|
+
oneTimeAmount,
|
|
146
|
+
unknownRecurrenceAmount,
|
|
147
|
+
forecastStatus: withheldAmount === '0' && unknownRecurrenceAmount === '0' ? 'available' : 'partial',
|
|
148
|
+
currencyCode: composition.accountingCurrencyCode,
|
|
149
|
+
...(reasonCodes.length === 0 ? {} : { reasonCodes: reasonCodes }),
|
|
150
|
+
...(forecastReasonCodes.length === 0
|
|
151
|
+
? {}
|
|
152
|
+
: { forecastReasonCodes: forecastReasonCodes }),
|
|
153
|
+
};
|
|
154
|
+
return result;
|
|
155
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatExactDecimalValue, parseCanonicalDecimal, subtractExactDecimalValues, sumCanonicalDecimals, } from '../common/exactDecimal.js';
|
|
2
2
|
import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
|
|
3
|
+
import { selectFinancialChargesV1 } from './financialChargeCompositionValidation.js';
|
|
3
4
|
import { createCurrentSpendCompositionIdV1, createCurrentSpendMembershipDigestV1, isCurrentSpendCompositionCompatibleV1, } from './financialDataflowValidation.js';
|
|
4
5
|
import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
|
|
5
6
|
export const CURRENT_SPEND_COMPOSITION_ALGORITHM_VERSION_V1 = 'current-spend-composition/shared-v1';
|
|
@@ -58,17 +59,50 @@ export const composeCurrentSpendV1 = (request) => {
|
|
|
58
59
|
}
|
|
59
60
|
if (request.baselines.length === 0)
|
|
60
61
|
throw new TypeError('Current-spend composition requires an authoritative baseline member.');
|
|
62
|
+
if (request.chargeCompositions.length > request.baselines.length) {
|
|
63
|
+
throw new TypeError('Current-spend composition has more charge compositions than baseline members.');
|
|
64
|
+
}
|
|
65
|
+
const chargeCompositionByBaselineId = new Map(request.chargeCompositions.map(composition => [composition.baselineId, composition]));
|
|
66
|
+
if (chargeCompositionByBaselineId.size !== request.chargeCompositions.length) {
|
|
67
|
+
throw new TypeError('Current-spend composition cannot contain duplicate charge compositions.');
|
|
68
|
+
}
|
|
61
69
|
const sortedBaselines = [...request.baselines].sort((left, right) => left.scopeId.localeCompare(right.scopeId));
|
|
62
70
|
if (new Set(sortedBaselines.map(baseline => baseline.scopeId)).size !== sortedBaselines.length) {
|
|
63
71
|
throw new TypeError('Current-spend composition cannot contain duplicate member scopes.');
|
|
64
72
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const selections = new Map();
|
|
74
|
+
const members = sortedBaselines.map(baseline => {
|
|
75
|
+
if (baseline.status === 'unavailable') {
|
|
76
|
+
return { memberScopeId: baseline.scopeId, status: 'unavailable', reasonCode: baseline.unavailableReason };
|
|
77
|
+
}
|
|
78
|
+
if (baseline.baselineKind !== 'owner') {
|
|
79
|
+
return { memberScopeId: baseline.scopeId, status: 'unavailable', reasonCode: 'charge-composition-requires-owner-baseline' };
|
|
80
|
+
}
|
|
81
|
+
const chargeComposition = chargeCompositionByBaselineId.get(baseline.baselineId);
|
|
82
|
+
if (!chargeComposition || chargeComposition.ownerScopeId !== baseline.scopeId) {
|
|
83
|
+
return { memberScopeId: baseline.scopeId, status: 'unavailable', reasonCode: 'charge-composition-unavailable' };
|
|
84
|
+
}
|
|
85
|
+
const selection = selectFinancialChargesV1(chargeComposition, request.coordinate.chargeInclusionPolicyRef);
|
|
86
|
+
selections.set(baseline.baselineId, selection);
|
|
87
|
+
return {
|
|
88
|
+
memberScopeId: baseline.scopeId,
|
|
89
|
+
baselineId: baseline.baselineId,
|
|
90
|
+
chargeCompositionId: chargeComposition.chargeCompositionId,
|
|
91
|
+
status: 'included',
|
|
92
|
+
};
|
|
93
|
+
});
|
|
68
94
|
const available = sortedBaselines.filter((baseline) => baseline.status === 'available');
|
|
95
|
+
const includedBaselineIds = new Set(members.flatMap(member => (member.status === 'included' ? [member.baselineId] : [])));
|
|
96
|
+
const includedBaselines = available.filter(baseline => includedBaselineIds.has(baseline.baselineId));
|
|
69
97
|
const unavailableReasons = new Set(sortedBaselines
|
|
70
98
|
.filter((baseline) => baseline.status === 'unavailable')
|
|
71
99
|
.map(baseline => baseline.unavailableReason));
|
|
100
|
+
members.filter(member => member.status === 'unavailable').forEach(member => unavailableReasons.add(member.reasonCode));
|
|
101
|
+
const selectionReasonCodes = new Set();
|
|
102
|
+
selections.forEach(selection => selection.reasonCodes?.forEach(reason => {
|
|
103
|
+
selectionReasonCodes.add(reason);
|
|
104
|
+
unavailableReasons.add(reason);
|
|
105
|
+
}));
|
|
72
106
|
if (available.some(baseline => !isCompleteFinancialBaselinePeriodV2(baseline.period)))
|
|
73
107
|
unavailableReasons.add('coverage-incomplete');
|
|
74
108
|
if (request.coordinate.accountingCurrency.status === 'unresolved') {
|
|
@@ -84,7 +118,7 @@ export const composeCurrentSpendV1 = (request) => {
|
|
|
84
118
|
};
|
|
85
119
|
const currency = request.coordinate.accountingCurrency.status === 'resolved' ? request.coordinate.accountingCurrency.currencyCode : undefined;
|
|
86
120
|
let identity;
|
|
87
|
-
if (!currency ||
|
|
121
|
+
if (!currency || includedBaselines.length === 0) {
|
|
88
122
|
identity = {
|
|
89
123
|
...common,
|
|
90
124
|
amount: {
|
|
@@ -94,17 +128,36 @@ export const composeCurrentSpendV1 = (request) => {
|
|
|
94
128
|
};
|
|
95
129
|
}
|
|
96
130
|
else {
|
|
97
|
-
const
|
|
131
|
+
const includedSelections = members.flatMap(member => member.status === 'included' ? [selections.get(member.baselineId)] : []);
|
|
132
|
+
const aggregateSelection = {
|
|
133
|
+
status: includedSelections.some(selection => selection.status === 'partial') ? 'partial' : 'available',
|
|
134
|
+
includedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.includedAmount))),
|
|
135
|
+
excludedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.excludedAmount))),
|
|
136
|
+
withheldAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.withheldAmount))),
|
|
137
|
+
forecastEligibleAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.forecastEligibleAmount))),
|
|
138
|
+
oneTimeAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.oneTimeAmount))),
|
|
139
|
+
unknownRecurrenceAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.unknownRecurrenceAmount))),
|
|
140
|
+
forecastStatus: includedSelections.some(selection => selection.forecastStatus === 'partial') ? 'partial' : 'available',
|
|
141
|
+
currencyCode: currency,
|
|
142
|
+
...(selectionReasonCodes.size === 0 ? {} : { reasonCodes: uniqueSorted(selectionReasonCodes) }),
|
|
143
|
+
...(includedSelections.every(selection => selection.forecastReasonCodes === undefined)
|
|
144
|
+
? {}
|
|
145
|
+
: {
|
|
146
|
+
forecastReasonCodes: uniqueSorted(includedSelections.flatMap(selection => selection.forecastReasonCodes ?? [])),
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
const knownAmount = aggregateSelection.includedAmount;
|
|
98
150
|
identity =
|
|
99
151
|
unavailableReasons.size === 0
|
|
100
|
-
? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency } }
|
|
152
|
+
? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency }, chargeSelection: aggregateSelection }
|
|
101
153
|
: {
|
|
102
154
|
...common,
|
|
103
155
|
amount: { status: 'partial', knownAmount, currencyCode: currency, reasonCodes: uniqueSorted(unavailableReasons) },
|
|
156
|
+
chargeSelection: aggregateSelection,
|
|
104
157
|
};
|
|
105
158
|
}
|
|
106
159
|
const composition = { ...identity, compositionId: createCurrentSpendCompositionIdV1(identity) };
|
|
107
|
-
if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines)) {
|
|
160
|
+
if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines, request.chargeCompositions)) {
|
|
108
161
|
throw new TypeError('Current-spend composition does not reconcile to its authoritative baseline members.');
|
|
109
162
|
}
|
|
110
163
|
return composition;
|
|
@@ -2,6 +2,8 @@ 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 { AZURE_BILLED_ALL_CHARGES_POLICY_V1, resolveFinancialChargeInclusionPolicyV1, } from './financialChargeComposition.js';
|
|
6
|
+
import { isFinancialChargeCompositionV1, selectFinancialChargesV1 } from './financialChargeCompositionValidation.js';
|
|
5
7
|
import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
|
|
6
8
|
import { isFinancialScopeBaselineEnvelopeV2 } from './financialScopeBaselineValidation.js';
|
|
7
9
|
import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
|
|
@@ -361,7 +363,18 @@ export const isFinancialDataflowScopeSelectorV1 = (value) => {
|
|
|
361
363
|
};
|
|
362
364
|
export const isFinancialDataflowCoordinateV1 = (value) => {
|
|
363
365
|
if (!isFinancialDataflowRecordV1(value) ||
|
|
364
|
-
!hasFinancialDataflowExactFieldsV1(value, [
|
|
366
|
+
!hasFinancialDataflowExactFieldsV1(value, [
|
|
367
|
+
'companyId',
|
|
368
|
+
'provider',
|
|
369
|
+
'providerAccountRefs',
|
|
370
|
+
'scope',
|
|
371
|
+
'periodRole',
|
|
372
|
+
'period',
|
|
373
|
+
'costBasis',
|
|
374
|
+
'estimateLens',
|
|
375
|
+
'accountingCurrency',
|
|
376
|
+
'chargeInclusionPolicyRef',
|
|
377
|
+
], ['requestedCurrencyCode']) ||
|
|
365
378
|
!isFinancialDataflowIdentityV1(value.companyId) ||
|
|
366
379
|
value.provider !== 'azure' ||
|
|
367
380
|
!isFinancialDataflowSortedUniqueStringsV1(value.providerAccountRefs, FINANCIAL_DATAFLOW_LIMITS_V1.maximumProviderAccounts) ||
|
|
@@ -375,7 +388,12 @@ export const isFinancialDataflowCoordinateV1 = (value) => {
|
|
|
375
388
|
typeof value.estimateLens !== 'string' ||
|
|
376
389
|
!ESTIMATE_LENSES.has(value.estimateLens) ||
|
|
377
390
|
(value.requestedCurrencyCode !== undefined && !isFinancialDataflowCurrencyV1(value.requestedCurrencyCode)) ||
|
|
378
|
-
!isFinancialDataflowRecordV1(value.accountingCurrency)
|
|
391
|
+
!isFinancialDataflowRecordV1(value.accountingCurrency) ||
|
|
392
|
+
!isFinancialDataflowRecordV1(value.chargeInclusionPolicyRef) ||
|
|
393
|
+
!hasFinancialDataflowExactFieldsV1(value.chargeInclusionPolicyRef, ['policyId', 'policyDigest']) ||
|
|
394
|
+
!isFinancialDataflowIdentityV1(value.chargeInclusionPolicyRef.policyId) ||
|
|
395
|
+
!isFinancialDataflowHashV1(value.chargeInclusionPolicyRef.policyDigest) ||
|
|
396
|
+
resolveFinancialChargeInclusionPolicyV1(value.chargeInclusionPolicyRef) === undefined) {
|
|
379
397
|
return false;
|
|
380
398
|
}
|
|
381
399
|
if (value.accountingCurrency.status === 'resolved') {
|
|
@@ -403,7 +421,9 @@ const isMember = (value) => {
|
|
|
403
421
|
if (!isFinancialDataflowRecordV1(value) || !isFinancialDataflowIdentityV1(value.memberScopeId))
|
|
404
422
|
return false;
|
|
405
423
|
if (value.status === 'included') {
|
|
406
|
-
return hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'baselineId', 'status']) &&
|
|
424
|
+
return (hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'baselineId', 'chargeCompositionId', 'status']) &&
|
|
425
|
+
isFinancialDataflowHashV1(value.baselineId) &&
|
|
426
|
+
isFinancialDataflowHashV1(value.chargeCompositionId));
|
|
407
427
|
}
|
|
408
428
|
return (value.status === 'unavailable' &&
|
|
409
429
|
hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'status', 'reasonCode']) &&
|
|
@@ -421,15 +441,7 @@ export const canonicalizeCurrentSpendMembershipV1 = (members) => {
|
|
|
421
441
|
export const createCurrentSpendMembershipDigestV1 = (members) => `sha256:${sha256Utf8(canonicalizeCurrentSpendMembershipV1(members))}`;
|
|
422
442
|
const isCompositionIdentity = (value) => {
|
|
423
443
|
if (!isFinancialDataflowRecordV1(value) ||
|
|
424
|
-
!hasFinancialDataflowExactFieldsV1(value, [
|
|
425
|
-
'schemaVersion',
|
|
426
|
-
'contractVersion',
|
|
427
|
-
'coordinate',
|
|
428
|
-
'members',
|
|
429
|
-
'amount',
|
|
430
|
-
'membershipDigest',
|
|
431
|
-
'algorithmVersion',
|
|
432
|
-
]) ||
|
|
444
|
+
!hasFinancialDataflowExactFieldsV1(value, ['schemaVersion', 'contractVersion', 'coordinate', 'members', 'amount', 'membershipDigest', 'algorithmVersion'], ['chargeSelection']) ||
|
|
433
445
|
value.schemaVersion !== FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1 ||
|
|
434
446
|
value.contractVersion !== FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1 ||
|
|
435
447
|
!isFinancialDataflowCoordinateV1(value.coordinate) ||
|
|
@@ -445,11 +457,45 @@ const isCompositionIdentity = (value) => {
|
|
|
445
457
|
return false;
|
|
446
458
|
}
|
|
447
459
|
const currency = value.coordinate.accountingCurrency.status === 'resolved' ? value.coordinate.accountingCurrency.currencyCode : undefined;
|
|
460
|
+
const isChargeSelection = (selection) => {
|
|
461
|
+
if (!isFinancialDataflowRecordV1(selection) ||
|
|
462
|
+
!hasFinancialDataflowExactFieldsV1(selection, [
|
|
463
|
+
'status',
|
|
464
|
+
'includedAmount',
|
|
465
|
+
'excludedAmount',
|
|
466
|
+
'withheldAmount',
|
|
467
|
+
'forecastEligibleAmount',
|
|
468
|
+
'oneTimeAmount',
|
|
469
|
+
'unknownRecurrenceAmount',
|
|
470
|
+
'forecastStatus',
|
|
471
|
+
'currencyCode',
|
|
472
|
+
], ['reasonCodes', 'forecastReasonCodes']) ||
|
|
473
|
+
(selection.status !== 'available' && selection.status !== 'partial') ||
|
|
474
|
+
(selection.forecastStatus !== 'available' && selection.forecastStatus !== 'partial') ||
|
|
475
|
+
!isFinancialDataflowCurrencyV1(selection.currencyCode) ||
|
|
476
|
+
![
|
|
477
|
+
selection.includedAmount,
|
|
478
|
+
selection.excludedAmount,
|
|
479
|
+
selection.withheldAmount,
|
|
480
|
+
selection.forecastEligibleAmount,
|
|
481
|
+
selection.oneTimeAmount,
|
|
482
|
+
selection.unknownRecurrenceAmount,
|
|
483
|
+
].every(amount => isCanonicalExactMoney({ amount, currencyCode: selection.currencyCode })))
|
|
484
|
+
return false;
|
|
485
|
+
return ((selection.status === 'available' ? selection.reasonCodes === undefined : isReasonCodes(selection.reasonCodes)) &&
|
|
486
|
+
(selection.forecastStatus === 'available'
|
|
487
|
+
? selection.forecastReasonCodes === undefined
|
|
488
|
+
: isReasonCodes(selection.forecastReasonCodes)));
|
|
489
|
+
};
|
|
448
490
|
if (value.amount.status === 'available') {
|
|
449
491
|
return (currency !== undefined &&
|
|
450
492
|
hasFinancialDataflowExactFieldsV1(value.amount, ['status', 'amount', 'currencyCode']) &&
|
|
451
493
|
isCanonicalExactMoney({ amount: value.amount.amount, currencyCode: value.amount.currencyCode }) &&
|
|
452
494
|
value.amount.currencyCode === currency &&
|
|
495
|
+
isChargeSelection(value.chargeSelection) &&
|
|
496
|
+
value.chargeSelection.status === 'available' &&
|
|
497
|
+
value.chargeSelection.currencyCode === currency &&
|
|
498
|
+
value.chargeSelection.includedAmount === value.amount.amount &&
|
|
453
499
|
value.members.length > 0 &&
|
|
454
500
|
value.members.every(member => member.status === 'included'));
|
|
455
501
|
}
|
|
@@ -459,11 +505,17 @@ const isCompositionIdentity = (value) => {
|
|
|
459
505
|
isCanonicalExactMoney({ amount: value.amount.knownAmount, currencyCode: value.amount.currencyCode }) &&
|
|
460
506
|
value.amount.currencyCode === currency &&
|
|
461
507
|
isReasonCodes(value.amount.reasonCodes) &&
|
|
508
|
+
isChargeSelection(value.chargeSelection) &&
|
|
509
|
+
value.chargeSelection.currencyCode === currency &&
|
|
510
|
+
value.chargeSelection.includedAmount === value.amount.knownAmount &&
|
|
462
511
|
value.members.some(member => member.status === 'included') &&
|
|
463
|
-
(value.members.some(member => member.status === 'unavailable') ||
|
|
512
|
+
(value.members.some(member => member.status === 'unavailable') ||
|
|
513
|
+
value.amount.reasonCodes.includes('coverage-incomplete') ||
|
|
514
|
+
value.chargeSelection.status === 'partial'));
|
|
464
515
|
}
|
|
465
516
|
const unresolvedCurrency = value.coordinate.accountingCurrency;
|
|
466
517
|
return (value.amount.status === 'unavailable' &&
|
|
518
|
+
value.chargeSelection === undefined &&
|
|
467
519
|
hasFinancialDataflowExactFieldsV1(value.amount, ['status', 'reasonCodes']) &&
|
|
468
520
|
isReasonCodes(value.amount.reasonCodes) &&
|
|
469
521
|
(currency !== undefined || (unresolvedCurrency.status === 'unresolved' && value.amount.reasonCodes.includes(unresolvedCurrency.reasonCode))));
|
|
@@ -480,6 +532,19 @@ export const canonicalizeCurrentSpendCompositionIdentityV1 = (value) => {
|
|
|
480
532
|
},
|
|
481
533
|
members: [...value.members].sort((left, right) => left.memberScopeId.localeCompare(right.memberScopeId)),
|
|
482
534
|
amount: value.amount.status === 'available' ? value.amount : { ...value.amount, reasonCodes: [...value.amount.reasonCodes].sort() },
|
|
535
|
+
...(value.chargeSelection === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: {
|
|
538
|
+
chargeSelection: {
|
|
539
|
+
...value.chargeSelection,
|
|
540
|
+
...(value.chargeSelection.reasonCodes === undefined
|
|
541
|
+
? {}
|
|
542
|
+
: { reasonCodes: [...value.chargeSelection.reasonCodes].sort() }),
|
|
543
|
+
...(value.chargeSelection.forecastReasonCodes === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { forecastReasonCodes: [...value.chargeSelection.forecastReasonCodes].sort() }),
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
483
548
|
};
|
|
484
549
|
return canonicalizeFinancialDataflowJsonV1(canonical);
|
|
485
550
|
};
|
|
@@ -496,7 +561,7 @@ export const isCurrentSpendCompositionV1 = (value) => {
|
|
|
496
561
|
'amount',
|
|
497
562
|
'membershipDigest',
|
|
498
563
|
'algorithmVersion',
|
|
499
|
-
])) {
|
|
564
|
+
], ['chargeSelection'])) {
|
|
500
565
|
return false;
|
|
501
566
|
}
|
|
502
567
|
const { compositionId, ...identity } = value;
|
|
@@ -511,65 +576,118 @@ const hasCompatibleBaselineCoordinate = (coordinate, baseline) => baseline.provi
|
|
|
511
576
|
toCanonicalEstimateLensV1(baseline.estimateLens) === coordinate.estimateLens &&
|
|
512
577
|
baseline.requestedCurrencyCode === coordinate.requestedCurrencyCode;
|
|
513
578
|
/** Proves that composition membership and money reconcile to exact V2 baseline envelopes. */
|
|
514
|
-
export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) => {
|
|
579
|
+
export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines, chargeCompositions) => {
|
|
515
580
|
if (!isCurrentSpendCompositionV1(composition) ||
|
|
516
581
|
!Array.isArray(baselines) ||
|
|
517
582
|
baselines.length !== composition.members.length ||
|
|
518
583
|
baselines.length > FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers ||
|
|
519
|
-
!baselines.every(isFinancialScopeBaselineEnvelopeV2)
|
|
584
|
+
!baselines.every(isFinancialScopeBaselineEnvelopeV2) ||
|
|
585
|
+
!Array.isArray(chargeCompositions) ||
|
|
586
|
+
!chargeCompositions.every(isFinancialChargeCompositionV1)) {
|
|
520
587
|
return false;
|
|
521
588
|
}
|
|
522
589
|
const typedBaselines = baselines;
|
|
590
|
+
const typedChargeCompositions = chargeCompositions;
|
|
591
|
+
const chargeCompositionById = new Map(typedChargeCompositions.map(value => [value.chargeCompositionId, value]));
|
|
592
|
+
if (chargeCompositionById.size !== typedChargeCompositions.length)
|
|
593
|
+
return false;
|
|
523
594
|
const baselineByScopeId = new Map(typedBaselines.map(baseline => [baseline.scopeId, baseline]));
|
|
524
595
|
if (baselineByScopeId.size !== typedBaselines.length ||
|
|
525
596
|
new Set(typedBaselines.filter(baseline => baseline.status === 'available').map(baseline => baseline.baselineId)).size !==
|
|
526
597
|
typedBaselines.filter(baseline => baseline.status === 'available').length) {
|
|
527
598
|
return false;
|
|
528
599
|
}
|
|
529
|
-
const
|
|
600
|
+
const includedSelections = [];
|
|
530
601
|
const expectedReasonCodes = new Set();
|
|
531
602
|
for (const member of composition.members) {
|
|
532
603
|
const baseline = baselineByScopeId.get(member.memberScopeId);
|
|
533
604
|
if (baseline === undefined || !hasCompatibleBaselineCoordinate(composition.coordinate, baseline))
|
|
534
605
|
return false;
|
|
535
606
|
if (member.status === 'included') {
|
|
536
|
-
if (baseline.status !== 'available' ||
|
|
607
|
+
if (baseline.status !== 'available' ||
|
|
608
|
+
baseline.baselineKind !== 'owner' ||
|
|
609
|
+
member.baselineId !== baseline.baselineId ||
|
|
610
|
+
canonicalizeFinancialDataflowJsonV1(baseline.chargeInclusionPolicyRef) !==
|
|
611
|
+
canonicalizeFinancialDataflowJsonV1(AZURE_BILLED_ALL_CHARGES_POLICY_V1.policyRef))
|
|
612
|
+
return false;
|
|
613
|
+
const chargeComposition = chargeCompositionById.get(member.chargeCompositionId);
|
|
614
|
+
if (chargeComposition === undefined ||
|
|
615
|
+
chargeComposition.baselineId !== baseline.baselineId ||
|
|
616
|
+
chargeComposition.ownerScopeId !== baseline.scopeId ||
|
|
617
|
+
chargeComposition.period.windowKind !== baseline.period.windowKind ||
|
|
618
|
+
canonicalizeFinancialDataflowJsonV1(chargeComposition.period.requested) !== canonicalizeFinancialDataflowJsonV1(baseline.period.requested) ||
|
|
619
|
+
chargeComposition.costBasis !== baseline.costBasis ||
|
|
620
|
+
chargeComposition.estimateLens !== baseline.estimateLens ||
|
|
621
|
+
chargeComposition.accountingCurrencyCode !== baseline.total.currencyCode ||
|
|
622
|
+
chargeComposition.reconciliation.sourceTotal !== baseline.total.amount)
|
|
537
623
|
return false;
|
|
538
624
|
if (composition.coordinate.accountingCurrency.status === 'resolved' &&
|
|
539
625
|
baseline.total.currencyCode !== composition.coordinate.accountingCurrency.currencyCode) {
|
|
540
626
|
return false;
|
|
541
627
|
}
|
|
542
|
-
|
|
628
|
+
includedSelections.push(selectFinancialChargesV1(chargeComposition, composition.coordinate.chargeInclusionPolicyRef));
|
|
629
|
+
includedSelections[includedSelections.length - 1].reasonCodes?.forEach(reason => expectedReasonCodes.add(reason));
|
|
543
630
|
if (baseline.status === 'available' && !isCompleteFinancialBaselinePeriodV2(baseline.period)) {
|
|
544
631
|
expectedReasonCodes.add('coverage-incomplete');
|
|
545
632
|
}
|
|
546
633
|
continue;
|
|
547
634
|
}
|
|
548
|
-
if (baseline.status
|
|
549
|
-
|
|
550
|
-
|
|
635
|
+
if (baseline.status === 'unavailable') {
|
|
636
|
+
if (member.reasonCode !== baseline.unavailableReason)
|
|
637
|
+
return false;
|
|
638
|
+
expectedReasonCodes.add(baseline.unavailableReason);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const expectedDerivedReason = baseline.baselineKind === 'aggregate'
|
|
642
|
+
? 'charge-composition-requires-owner-baseline'
|
|
643
|
+
: 'charge-composition-unavailable';
|
|
644
|
+
if (member.reasonCode !== expectedDerivedReason)
|
|
645
|
+
return false;
|
|
646
|
+
expectedReasonCodes.add(expectedDerivedReason);
|
|
647
|
+
}
|
|
551
648
|
}
|
|
552
649
|
if (composition.coordinate.accountingCurrency.status === 'unresolved') {
|
|
553
650
|
expectedReasonCodes.add(composition.coordinate.accountingCurrency.reasonCode);
|
|
554
651
|
}
|
|
652
|
+
if (typedChargeCompositions.length !== includedSelections.length)
|
|
653
|
+
return false;
|
|
555
654
|
const hasExpectedReasonCodes = (reasonCodes) => reasonCodes.length === expectedReasonCodes.size && reasonCodes.every(reasonCode => expectedReasonCodes.has(reasonCode));
|
|
556
655
|
if (composition.coordinate.accountingCurrency.status === 'unresolved') {
|
|
557
656
|
return composition.amount.status === 'unavailable' && hasExpectedReasonCodes(composition.amount.reasonCodes);
|
|
558
657
|
}
|
|
559
|
-
if (
|
|
658
|
+
if (includedSelections.length === 0) {
|
|
560
659
|
return composition.amount.status === 'unavailable' && hasExpectedReasonCodes(composition.amount.reasonCodes);
|
|
561
660
|
}
|
|
562
|
-
let
|
|
661
|
+
let aggregateSelection;
|
|
563
662
|
try {
|
|
564
|
-
|
|
663
|
+
const reasonCodes = new Set(includedSelections.flatMap(selection => selection.reasonCodes ?? []));
|
|
664
|
+
const forecastReasonCodes = new Set(includedSelections.flatMap(selection => selection.forecastReasonCodes ?? []));
|
|
665
|
+
aggregateSelection = {
|
|
666
|
+
status: includedSelections.some(selection => selection.status === 'partial') ? 'partial' : 'available',
|
|
667
|
+
includedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.includedAmount))),
|
|
668
|
+
excludedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.excludedAmount))),
|
|
669
|
+
withheldAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.withheldAmount))),
|
|
670
|
+
forecastEligibleAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.forecastEligibleAmount))),
|
|
671
|
+
oneTimeAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.oneTimeAmount))),
|
|
672
|
+
unknownRecurrenceAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.unknownRecurrenceAmount))),
|
|
673
|
+
forecastStatus: includedSelections.some(selection => selection.forecastStatus === 'partial') ? 'partial' : 'available',
|
|
674
|
+
currencyCode: composition.coordinate.accountingCurrency.currencyCode,
|
|
675
|
+
...(reasonCodes.size === 0 ? {} : { reasonCodes: [...reasonCodes].sort() }),
|
|
676
|
+
...(forecastReasonCodes.size === 0
|
|
677
|
+
? {}
|
|
678
|
+
: { forecastReasonCodes: [...forecastReasonCodes].sort() }),
|
|
679
|
+
};
|
|
565
680
|
}
|
|
566
681
|
catch {
|
|
567
682
|
return false;
|
|
568
683
|
}
|
|
569
|
-
|
|
570
|
-
|
|
684
|
+
const sameSelection = canonicalizeFinancialDataflowJsonV1(composition.chargeSelection) === canonicalizeFinancialDataflowJsonV1(aggregateSelection);
|
|
685
|
+
if (!sameSelection)
|
|
686
|
+
return false;
|
|
687
|
+
if (includedSelections.length === composition.members.length && expectedReasonCodes.size === 0) {
|
|
688
|
+
return composition.amount.status === 'available' && composition.amount.amount === aggregateSelection.includedAmount;
|
|
571
689
|
}
|
|
572
690
|
return (composition.amount.status === 'partial' &&
|
|
573
|
-
composition.amount.knownAmount ===
|
|
691
|
+
composition.amount.knownAmount === aggregateSelection.includedAmount &&
|
|
574
692
|
hasExpectedReasonCodes(composition.amount.reasonCodes));
|
|
575
693
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { multiplyExactDecimalValues, parseCanonicalDecimal, subtractExactDecimalValues } from '../common/exactDecimal.js';
|
|
2
2
|
import { sha256Utf8 } from '../common/sha256.js';
|
|
3
3
|
import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
|
|
4
|
+
import { isFinancialChargeInclusionPolicyRefV1 } from './financialChargeCompositionValidation.js';
|
|
4
5
|
import { FINANCIAL_POLICY_DEFINITION_CONTRACT_VERSION_V1, FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1, } from './financialPolicy.js';
|
|
5
6
|
import { FINANCIAL_DATAFLOW_LIMITS_V1, canonicalizeFinancialDataflowJsonV1, canonicalizeFinancialDataflowCoordinateV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowCurrencyV1, createFinancialDataflowCoordinateIdV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowIsoInstantV1, isFinancialDataflowRecordV1, isFinancialDataflowScopeSelectorV1, isFinancialDataflowSortedUniqueStringsV1, isFinancialDataflowValueWithinLimitsV1, } from './financialDataflowValidation.js';
|
|
6
7
|
import { isCurrentSpendCompositionV1 } from './financialDataflowValidation.js';
|
|
@@ -85,6 +86,7 @@ const isCoordinateRequest = (value, companyId) => isFinancialDataflowRecordV1(va
|
|
|
85
86
|
'period',
|
|
86
87
|
'costBasis',
|
|
87
88
|
'estimateLens',
|
|
89
|
+
'chargeInclusionPolicyRef',
|
|
88
90
|
'requiredAccountingCurrencyCode',
|
|
89
91
|
]) &&
|
|
90
92
|
value.provider === 'azure' &&
|
|
@@ -107,6 +109,7 @@ const isCoordinateRequest = (value, companyId) => isFinancialDataflowRecordV1(va
|
|
|
107
109
|
COST_BASES.has(value.costBasis) &&
|
|
108
110
|
typeof value.estimateLens === 'string' &&
|
|
109
111
|
ESTIMATE_LENSES.has(value.estimateLens) &&
|
|
112
|
+
isFinancialChargeInclusionPolicyRefV1(value.chargeInclusionPolicyRef) &&
|
|
110
113
|
isFinancialDataflowCurrencyV1(value.requiredAccountingCurrencyCode) &&
|
|
111
114
|
isFinancialDataflowIdentityV1(companyId);
|
|
112
115
|
const isCriteria = (value, currencyCode) => {
|
|
@@ -418,6 +421,8 @@ export const isFinancialPolicyEvaluationCompatibleV1 = (evaluation, definition,
|
|
|
418
421
|
request.scope.scopeId !== coordinate.scope.scopeId ||
|
|
419
422
|
request.costBasis !== coordinate.costBasis ||
|
|
420
423
|
request.estimateLens !== coordinate.estimateLens ||
|
|
424
|
+
canonicalizeFinancialDataflowJsonV1(request.chargeInclusionPolicyRef) !==
|
|
425
|
+
canonicalizeFinancialDataflowJsonV1(coordinate.chargeInclusionPolicyRef) ||
|
|
421
426
|
coordinate.requestedCurrencyCode !== request.requiredAccountingCurrencyCode ||
|
|
422
427
|
(currency === undefined
|
|
423
428
|
? currentSpendComposition.amount.status !== 'unavailable'
|