@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
|
@@ -30,6 +30,7 @@ const comparableCoordinate = (value) => ({
|
|
|
30
30
|
estimateLens: value.estimateLens,
|
|
31
31
|
requestedCurrencyCode: value.requestedCurrencyCode,
|
|
32
32
|
accountingCurrency: value.accountingCurrency,
|
|
33
|
+
chargeInclusionPolicyRef: value.chargeInclusionPolicyRef,
|
|
33
34
|
});
|
|
34
35
|
const hasSameFinancialDimensions = (left, right) => canonicalizeFinancialDataflowJsonV1(comparableCoordinate(left)) === canonicalizeFinancialDataflowJsonV1(comparableCoordinate(right));
|
|
35
36
|
const hasComparableTrendPeriods = (current, comparison) => {
|
|
@@ -105,16 +106,47 @@ const isInputIdentity = (value) => {
|
|
|
105
106
|
return false;
|
|
106
107
|
}
|
|
107
108
|
dates.add(point.date);
|
|
109
|
+
const pointForecastFields = [
|
|
110
|
+
point.forecastEligibleAmount,
|
|
111
|
+
point.oneTimeAmount,
|
|
112
|
+
point.unknownRecurrenceAmount,
|
|
113
|
+
];
|
|
114
|
+
if (!pointForecastFields.every(amount => isCanonicalExactMoney({ amount, currencyCode: coordinate.accountingCurrency.currencyCode })) ||
|
|
115
|
+
(point.forecastStatus !== 'available' && point.forecastStatus !== 'partial') ||
|
|
116
|
+
(point.forecastStatus === 'available'
|
|
117
|
+
? point.forecastReasonCodes !== undefined || point.unknownRecurrenceAmount !== '0'
|
|
118
|
+
: !isReasonCodes(point.forecastReasonCodes))) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
108
121
|
if (point.status === 'available') {
|
|
109
|
-
if (!hasFinancialDataflowExactFieldsV1(point, [
|
|
110
|
-
|
|
122
|
+
if (!hasFinancialDataflowExactFieldsV1(point, [
|
|
123
|
+
'date',
|
|
124
|
+
'compositionId',
|
|
125
|
+
'status',
|
|
126
|
+
'amount',
|
|
127
|
+
'forecastEligibleAmount',
|
|
128
|
+
'oneTimeAmount',
|
|
129
|
+
'unknownRecurrenceAmount',
|
|
130
|
+
'forecastStatus',
|
|
131
|
+
], ['forecastReasonCodes']) ||
|
|
132
|
+
!isCanonicalExactMoney({ amount: point.amount, currencyCode: coordinate.accountingCurrency.currencyCode }))
|
|
111
133
|
return false;
|
|
112
134
|
availableDayCount += 1;
|
|
113
135
|
}
|
|
114
136
|
else {
|
|
115
137
|
if (point.status !== 'partial' ||
|
|
116
|
-
!hasFinancialDataflowExactFieldsV1(point, [
|
|
117
|
-
|
|
138
|
+
!hasFinancialDataflowExactFieldsV1(point, [
|
|
139
|
+
'date',
|
|
140
|
+
'compositionId',
|
|
141
|
+
'status',
|
|
142
|
+
'knownAmount',
|
|
143
|
+
'reasonCodes',
|
|
144
|
+
'forecastEligibleAmount',
|
|
145
|
+
'oneTimeAmount',
|
|
146
|
+
'unknownRecurrenceAmount',
|
|
147
|
+
'forecastStatus',
|
|
148
|
+
], ['forecastReasonCodes']) ||
|
|
149
|
+
!isCanonicalExactMoney({ amount: point.knownAmount, currencyCode: coordinate.accountingCurrency.currencyCode }) ||
|
|
118
150
|
!isReasonCodes(point.reasonCodes))
|
|
119
151
|
return false;
|
|
120
152
|
partialDayCount += 1;
|
|
@@ -174,7 +206,13 @@ export const canonicalizeFinancialAnalyticsInputIdentityV1 = (value) => {
|
|
|
174
206
|
referenceCompositions: [...value.referenceCompositions].sort((left, right) => left.compositionId.localeCompare(right.compositionId)),
|
|
175
207
|
points: [...value.points]
|
|
176
208
|
.sort((left, right) => left.date.localeCompare(right.date))
|
|
177
|
-
.map(point => (
|
|
209
|
+
.map(point => ({
|
|
210
|
+
...point,
|
|
211
|
+
...(point.status === 'partial' ? { reasonCodes: [...point.reasonCodes].sort() } : {}),
|
|
212
|
+
...(point.forecastReasonCodes === undefined
|
|
213
|
+
? {}
|
|
214
|
+
: { forecastReasonCodes: [...point.forecastReasonCodes].sort() }),
|
|
215
|
+
})),
|
|
178
216
|
gaps: [...value.gaps]
|
|
179
217
|
.sort((left, right) => `${left.startDate}\u0000${left.endDateExclusive}`.localeCompare(`${right.startDate}\u0000${right.endDateExclusive}`))
|
|
180
218
|
.map(gap => ({ ...gap, reasonCodes: [...gap.reasonCodes].sort() })),
|
|
@@ -191,6 +229,18 @@ export const isFinancialAnalyticsInputSeriesV1 = (value) => {
|
|
|
191
229
|
};
|
|
192
230
|
const nextCalendarDate = (value) => new Date(Date.parse(`${value}T00:00:00.000Z`) + 86400000).toISOString().slice(0, 10);
|
|
193
231
|
const hasSameReasonCodes = (left, right) => left.length === right.length && left.every(reasonCode => right.includes(reasonCode));
|
|
232
|
+
const hasSameForecastSlice = (point, composition) => {
|
|
233
|
+
const selection = composition.chargeSelection;
|
|
234
|
+
return selection !== undefined &&
|
|
235
|
+
point.forecastEligibleAmount === selection.forecastEligibleAmount &&
|
|
236
|
+
point.oneTimeAmount === selection.oneTimeAmount &&
|
|
237
|
+
point.unknownRecurrenceAmount === selection.unknownRecurrenceAmount &&
|
|
238
|
+
point.forecastStatus === selection.forecastStatus &&
|
|
239
|
+
(point.forecastReasonCodes === undefined
|
|
240
|
+
? selection.forecastReasonCodes === undefined
|
|
241
|
+
: selection.forecastReasonCodes !== undefined &&
|
|
242
|
+
hasSameReasonCodes(point.forecastReasonCodes, selection.forecastReasonCodes));
|
|
243
|
+
};
|
|
194
244
|
/** Proves that every analytics point is an exact projection of its referenced daily composition. */
|
|
195
245
|
export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyCompositions) => {
|
|
196
246
|
if (!isFinancialAnalyticsInputSeriesV1(input) ||
|
|
@@ -206,6 +256,7 @@ export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyComposit
|
|
|
206
256
|
return input.points.every(point => {
|
|
207
257
|
const composition = compositionById.get(point.compositionId);
|
|
208
258
|
if (composition === undefined ||
|
|
259
|
+
composition.amount.status === 'unavailable' ||
|
|
209
260
|
composition.coordinate.periodRole !== 'current-spend' ||
|
|
210
261
|
composition.coordinate.period.windowKind !== 'daily' ||
|
|
211
262
|
composition.coordinate.period.requested.startDate !== point.date ||
|
|
@@ -214,11 +265,14 @@ export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyComposit
|
|
|
214
265
|
return false;
|
|
215
266
|
}
|
|
216
267
|
if (point.status === 'available') {
|
|
217
|
-
return composition.amount.status === 'available' &&
|
|
268
|
+
return (composition.amount.status === 'available' &&
|
|
269
|
+
point.amount === composition.amount.amount &&
|
|
270
|
+
hasSameForecastSlice(point, composition));
|
|
218
271
|
}
|
|
219
272
|
return (composition.amount.status === 'partial' &&
|
|
220
273
|
point.knownAmount === composition.amount.knownAmount &&
|
|
221
|
-
hasSameReasonCodes(point.reasonCodes, composition.amount.reasonCodes)
|
|
274
|
+
hasSameReasonCodes(point.reasonCodes, composition.amount.reasonCodes) &&
|
|
275
|
+
hasSameForecastSlice(point, composition));
|
|
222
276
|
});
|
|
223
277
|
};
|
|
224
278
|
const isResult = (value, currency) => {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { FINANCIAL_AUTHORITY_RESOURCE_PROJECTION_CONTRACT_VERSION_V1, } from './financialAuthorityView.js';
|
|
2
|
+
import { FINANCIAL_SAVINGS_RESOURCE_PROJECTION_CONTRACT_VERSION_V1, } from './financialSavingsAuthority.js';
|
|
3
|
+
const normalize = (value) => value.trim().toLowerCase().replace(/\/+$/, '');
|
|
4
|
+
/** Derives one compact, non-additive resource view from a validated canonical authority. */
|
|
5
|
+
export const projectFinancialAuthorityResourceV1 = (authority, resourceType, scopeId) => {
|
|
6
|
+
const normalizedType = normalize(resourceType);
|
|
7
|
+
const normalizedScopeId = normalize(scopeId);
|
|
8
|
+
const coverage = authority.scopeCoverage.find(entry => normalize(entry.resourceType) === normalizedType && entry.scopeIds.some(candidate => normalize(candidate) === normalizedScopeId));
|
|
9
|
+
if (!coverage)
|
|
10
|
+
return undefined;
|
|
11
|
+
const coordinates = authority.coordinates.map(coordinate => {
|
|
12
|
+
const owners = coordinate.ownerBaselines.filter(baseline => normalize(baseline.scopeId) === normalizedScopeId);
|
|
13
|
+
const ownerBaseline = owners[0];
|
|
14
|
+
if (owners.length !== 1 || !ownerBaseline)
|
|
15
|
+
throw new TypeError('Financial authority resource owner is missing or ambiguous.');
|
|
16
|
+
const chargeComposition = ownerBaseline.status === 'available' && ownerBaseline.baselineKind === 'owner'
|
|
17
|
+
? coordinate.chargeCompositions.find(composition => composition.baselineId === ownerBaseline.baselineId)
|
|
18
|
+
: undefined;
|
|
19
|
+
if (ownerBaseline.status === 'available' && ownerBaseline.baselineKind === 'owner' && !chargeComposition) {
|
|
20
|
+
throw new TypeError('Financial authority resource charge composition is missing.');
|
|
21
|
+
}
|
|
22
|
+
const displayRollups = coordinate.displayRollups.filter(rollup => normalize(rollup.displayScopeId) === normalizedScopeId);
|
|
23
|
+
const displayMemberships = new Set(displayRollups.flatMap(rollup => rollup.members.map(member => `${member.baselineId}\u0000${member.componentId}`)));
|
|
24
|
+
return {
|
|
25
|
+
coordinateId: coordinate.coordinateId,
|
|
26
|
+
periodRole: coordinate.periodRole,
|
|
27
|
+
period: coordinate.period,
|
|
28
|
+
costBasis: coordinate.costBasis,
|
|
29
|
+
estimateLens: coordinate.estimateLens,
|
|
30
|
+
...(coordinate.requestedCurrencyCode === undefined ? {} : { requestedCurrencyCode: coordinate.requestedCurrencyCode }),
|
|
31
|
+
ownerBaseline,
|
|
32
|
+
...(chargeComposition === undefined ? {} : { chargeComposition }),
|
|
33
|
+
componentDescriptors: coordinate.componentDescriptors.filter(descriptor => displayMemberships.has(`${descriptor.baselineId}\u0000${descriptor.componentId}`)),
|
|
34
|
+
displayRollups,
|
|
35
|
+
projections: coordinate.projections.filter(projection => normalize(projection.scopeId) === normalizedScopeId),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
const firstCoordinate = coordinates[0];
|
|
39
|
+
if (!firstCoordinate)
|
|
40
|
+
throw new TypeError('Financial authority resource projection has no coordinates.');
|
|
41
|
+
const assessmentIds = new Set();
|
|
42
|
+
const bundleIds = new Set();
|
|
43
|
+
coordinates.forEach(coordinate => {
|
|
44
|
+
assessmentIds.add(coordinate.ownerBaseline.assessmentId);
|
|
45
|
+
if (coordinate.ownerBaseline.status === 'available' && coordinate.ownerBaseline.baselineKind === 'owner') {
|
|
46
|
+
bundleIds.add(coordinate.ownerBaseline.evidenceBundleId);
|
|
47
|
+
}
|
|
48
|
+
coordinate.projections.forEach(projection => {
|
|
49
|
+
if (projection.targetAssessmentId)
|
|
50
|
+
assessmentIds.add(projection.targetAssessmentId);
|
|
51
|
+
if (projection.targetEvidenceBundleId)
|
|
52
|
+
bundleIds.add(projection.targetEvidenceBundleId);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
const evidenceAssessments = authority.evidenceAssessments.filter(assessment => assessmentIds.has(assessment.assessmentId));
|
|
56
|
+
evidenceAssessments.forEach(assessment => {
|
|
57
|
+
if (assessment.evidenceBundleId)
|
|
58
|
+
bundleIds.add(assessment.evidenceBundleId);
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
contractVersion: FINANCIAL_AUTHORITY_RESOURCE_PROJECTION_CONTRACT_VERSION_V1,
|
|
62
|
+
authorityId: authority.authorityId,
|
|
63
|
+
provider: authority.provider,
|
|
64
|
+
providerAccountRefs: authority.providerAccountRefs,
|
|
65
|
+
artifactGeneration: authority.artifactGeneration,
|
|
66
|
+
billingGenerationId: authority.billingGenerationId,
|
|
67
|
+
resourceType: coverage.resourceType,
|
|
68
|
+
financialRole: coverage.financialRole,
|
|
69
|
+
scopeId: normalizedScopeId,
|
|
70
|
+
evidenceBundles: authority.evidenceBundles.filter(bundle => bundleIds.has(bundle.bundleId)),
|
|
71
|
+
evidenceAssessments,
|
|
72
|
+
coordinates: [firstCoordinate, ...coordinates.slice(1)],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** Derives the matching compact savings view for one canonical resource owner. */
|
|
76
|
+
export const projectFinancialSavingsResourceV1 = (savingsAuthority, financialProjection, financialAuthority) => {
|
|
77
|
+
if (savingsAuthority.financialAuthorityId !== financialProjection.authorityId ||
|
|
78
|
+
savingsAuthority.artifactGeneration.runId !== financialProjection.artifactGeneration.runId ||
|
|
79
|
+
savingsAuthority.artifactGeneration.generatedAt !== financialProjection.artifactGeneration.generatedAt) {
|
|
80
|
+
throw new TypeError('Financial savings authority is not bound to the resource Financial Authority projection.');
|
|
81
|
+
}
|
|
82
|
+
if (financialAuthority !== undefined &&
|
|
83
|
+
(financialAuthority.authorityId !== financialProjection.authorityId ||
|
|
84
|
+
financialAuthority.artifactGeneration.runId !== financialProjection.artifactGeneration.runId ||
|
|
85
|
+
financialAuthority.artifactGeneration.generatedAt !== financialProjection.artifactGeneration.generatedAt)) {
|
|
86
|
+
throw new TypeError('Full Financial Authority is not bound to the resource projection.');
|
|
87
|
+
}
|
|
88
|
+
const normalizedScopeId = normalize(financialProjection.scopeId);
|
|
89
|
+
const financialCoordinateIds = new Set(financialProjection.coordinates.map(coordinate => coordinate.coordinateId));
|
|
90
|
+
const savingsByCoordinateId = new Map(savingsAuthority.coordinates.map(coordinate => [coordinate.coordinateId, coordinate]));
|
|
91
|
+
if (financialCoordinateIds.size !== financialProjection.coordinates.length ||
|
|
92
|
+
savingsAuthority.coordinates.length !== financialProjection.coordinates.length ||
|
|
93
|
+
savingsAuthority.coordinates.some(coordinate => !financialCoordinateIds.has(coordinate.coordinateId))) {
|
|
94
|
+
throw new TypeError('Financial savings coordinates do not match the resource Financial Authority projection.');
|
|
95
|
+
}
|
|
96
|
+
const coordinates = financialProjection.coordinates.map(financialCoordinate => {
|
|
97
|
+
const coordinate = savingsByCoordinateId.get(financialCoordinate.coordinateId);
|
|
98
|
+
if (!coordinate)
|
|
99
|
+
throw new TypeError('Financial savings coordinate is missing.');
|
|
100
|
+
if (coordinate.status === 'unavailable')
|
|
101
|
+
return { ...coordinate };
|
|
102
|
+
const resourceContributions = coordinate.resourceContributions.filter(contribution => normalize(contribution.ownerScopeId) === normalizedScopeId);
|
|
103
|
+
if (resourceContributions.length > 1)
|
|
104
|
+
throw new TypeError('Financial savings resource contribution is ambiguous.');
|
|
105
|
+
const recommendationContributions = coordinate.recommendationContributions.filter(contribution => normalize(contribution.ownerScopeId) === normalizedScopeId);
|
|
106
|
+
const resourceContribution = resourceContributions[0];
|
|
107
|
+
const recommendationSavings = recommendationContributions.reduce((total, contribution) => {
|
|
108
|
+
const next = total + contribution.savingsMinorUnits;
|
|
109
|
+
if (!Number.isSafeInteger(next))
|
|
110
|
+
throw new TypeError('Financial savings recommendation contribution overflows safe minor units.');
|
|
111
|
+
return next;
|
|
112
|
+
}, 0);
|
|
113
|
+
if ((resourceContribution?.savingsMinorUnits ?? 0) !== recommendationSavings) {
|
|
114
|
+
throw new TypeError('Financial savings recommendation contributions do not reconcile to the resource contribution.');
|
|
115
|
+
}
|
|
116
|
+
const unavailableActivationIds = new Set(coordinate.status === 'partial'
|
|
117
|
+
? coordinate.activations.filter(activation => activation.result === 'unavailable').map(activation => activation.scenarioId)
|
|
118
|
+
: []);
|
|
119
|
+
const authorityCoordinate = financialAuthority?.coordinates.find(candidate => candidate.coordinateId === financialCoordinate.coordinateId);
|
|
120
|
+
if (financialAuthority !== undefined && !authorityCoordinate) {
|
|
121
|
+
throw new TypeError('Full Financial Authority coordinate is missing for the resource projection.');
|
|
122
|
+
}
|
|
123
|
+
const unavailableScenarioIds = [...unavailableActivationIds]
|
|
124
|
+
.filter(scenarioId => {
|
|
125
|
+
if (!authorityCoordinate) {
|
|
126
|
+
// Without the full authority there is no proof that an absent target
|
|
127
|
+
// projection belongs elsewhere. Preserve the gap on this resource.
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const scenarioProjections = authorityCoordinate.projections.filter(projection => projection.scenarioId === scenarioId);
|
|
131
|
+
return (scenarioProjections.length === 0 ||
|
|
132
|
+
scenarioProjections.some(projection => normalize(projection.scopeId) === normalizedScopeId));
|
|
133
|
+
})
|
|
134
|
+
.sort();
|
|
135
|
+
const composed = {
|
|
136
|
+
coordinateId: coordinate.coordinateId,
|
|
137
|
+
currentAggregateBaselineId: coordinate.currentAggregateBaselineId,
|
|
138
|
+
accountingCurrencyCode: coordinate.accountingCurrencyCode,
|
|
139
|
+
minorUnitScale: coordinate.minorUnitScale,
|
|
140
|
+
roundingMode: coordinate.roundingMode,
|
|
141
|
+
...(resourceContribution === undefined ? {} : { resourceContribution }),
|
|
142
|
+
recommendationContributions,
|
|
143
|
+
};
|
|
144
|
+
return unavailableScenarioIds.length > 0
|
|
145
|
+
? { ...composed, status: 'partial', unavailableScenarioIds: unavailableScenarioIds }
|
|
146
|
+
: { ...composed, status: 'available' };
|
|
147
|
+
});
|
|
148
|
+
const firstCoordinate = coordinates[0];
|
|
149
|
+
if (!firstCoordinate)
|
|
150
|
+
throw new TypeError('Financial savings resource projection has no coordinates.');
|
|
151
|
+
return {
|
|
152
|
+
contractVersion: FINANCIAL_SAVINGS_RESOURCE_PROJECTION_CONTRACT_VERSION_V1,
|
|
153
|
+
savingsAuthorityId: savingsAuthority.savingsAuthorityId,
|
|
154
|
+
financialAuthorityId: savingsAuthority.financialAuthorityId,
|
|
155
|
+
artifactGeneration: savingsAuthority.artifactGeneration,
|
|
156
|
+
scopeId: normalizedScopeId,
|
|
157
|
+
coordinates: [firstCoordinate, ...coordinates.slice(1)],
|
|
158
|
+
};
|
|
159
|
+
};
|
|
@@ -5,6 +5,8 @@ import { isFinancialEvidenceAssessmentV1 } from './financialEvidenceAssessmentVa
|
|
|
5
5
|
import { formatExactDecimalValue, multiplyExactDecimalValues, parseCanonicalDecimal, sumCanonicalDecimals } from '../common/exactDecimal.js';
|
|
6
6
|
import { isFinancialBaselinePeriodV2, isFinancialEvidenceBundleV1, isFinancialScopeBaselineEnvelopeV2 } from './financialScopeBaselineValidation.js';
|
|
7
7
|
import { isFinancialAuthorityComponentDescriptorV1, isFinancialDisplayRollupV1 } from './financialDisplayRollupValidation.js';
|
|
8
|
+
import { AZURE_BILLED_ALL_CHARGES_POLICY_V1 } from './financialChargeComposition.js';
|
|
9
|
+
import { isFinancialChargeCompositionV1 } from './financialChargeCompositionValidation.js';
|
|
8
10
|
const SHA256_ID = /^sha256:[0-9a-f]{64}$/;
|
|
9
11
|
const CURRENCY = /^[A-Z]{3}$/;
|
|
10
12
|
const AZURE_RESOURCE_TYPE = /^[a-z0-9.-]+\/[a-z0-9._/-]+$/;
|
|
@@ -55,6 +57,7 @@ const canonicalCoordinatePreimage = (value) => ({
|
|
|
55
57
|
ownerBaselines: [...value.ownerBaselines].sort((left, right) => left.scopeId.localeCompare(right.scopeId)),
|
|
56
58
|
residualBaseline: value.residualBaseline,
|
|
57
59
|
aggregateBaseline: value.aggregateBaseline,
|
|
60
|
+
chargeCompositions: [...value.chargeCompositions].sort((left, right) => left.baselineId.localeCompare(right.baselineId)),
|
|
58
61
|
componentDescriptors: [...value.componentDescriptors].sort((left, right) => `${left.baselineId}\u0000${left.componentId}`.localeCompare(`${right.baselineId}\u0000${right.componentId}`)),
|
|
59
62
|
displayRollups: [...value.displayRollups].sort((left, right) => left.displayRollupId.localeCompare(right.displayRollupId)),
|
|
60
63
|
projections: [...value.projections].sort((left, right) => left.scenarioId.localeCompare(right.scenarioId)),
|
|
@@ -95,6 +98,7 @@ const isCoordinate = (value, coveredScopeIds, financialRoleByScope, providerAcco
|
|
|
95
98
|
'ownerBaselines',
|
|
96
99
|
'residualBaseline',
|
|
97
100
|
'aggregateBaseline',
|
|
101
|
+
'chargeCompositions',
|
|
98
102
|
'componentDescriptors',
|
|
99
103
|
'displayRollups',
|
|
100
104
|
'projections',
|
|
@@ -113,6 +117,9 @@ const isCoordinate = (value, coveredScopeIds, financialRoleByScope, providerAcco
|
|
|
113
117
|
!value.ownerBaselines.every(isFinancialScopeBaselineEnvelopeV2) ||
|
|
114
118
|
!isFinancialScopeBaselineEnvelopeV2(value.residualBaseline) ||
|
|
115
119
|
!isFinancialScopeBaselineEnvelopeV2(value.aggregateBaseline) ||
|
|
120
|
+
!Array.isArray(value.chargeCompositions) ||
|
|
121
|
+
value.chargeCompositions.length > 20001 ||
|
|
122
|
+
!value.chargeCompositions.every(isFinancialChargeCompositionV1) ||
|
|
116
123
|
!Array.isArray(value.componentDescriptors) ||
|
|
117
124
|
value.componentDescriptors.length > 20000 ||
|
|
118
125
|
!value.componentDescriptors.every(isFinancialAuthorityComponentDescriptorV1) ||
|
|
@@ -173,6 +180,38 @@ const isCoordinate = (value, coveredScopeIds, financialRoleByScope, providerAcco
|
|
|
173
180
|
}
|
|
174
181
|
if (coordinate.residualBaseline.status === 'available' && !bundleIds.has(coordinate.residualBaseline.evidenceBundleId))
|
|
175
182
|
return false;
|
|
183
|
+
const composableBaselines = [...coordinate.ownerBaselines, coordinate.residualBaseline].filter((baseline) => baseline.status === 'available' && baseline.baselineKind === 'owner');
|
|
184
|
+
const chargeCompositionByBaselineId = new Map(coordinate.chargeCompositions.map(composition => [composition.baselineId, composition]));
|
|
185
|
+
if (chargeCompositionByBaselineId.size !== coordinate.chargeCompositions.length ||
|
|
186
|
+
coordinate.chargeCompositions.length !== composableBaselines.length)
|
|
187
|
+
return false;
|
|
188
|
+
for (const baseline of composableBaselines) {
|
|
189
|
+
const composition = chargeCompositionByBaselineId.get(baseline.baselineId);
|
|
190
|
+
if (composition === undefined ||
|
|
191
|
+
canonicalText(baseline.chargeInclusionPolicyRef) !== canonicalText(AZURE_BILLED_ALL_CHARGES_POLICY_V1.policyRef) ||
|
|
192
|
+
composition.ownerScopeId !== baseline.scopeId ||
|
|
193
|
+
periodText(composition.period) !== periodText(baseline.period) ||
|
|
194
|
+
composition.costBasis !== baseline.costBasis ||
|
|
195
|
+
composition.estimateLens !== baseline.estimateLens ||
|
|
196
|
+
composition.accountingCurrencyCode !== baseline.total.currencyCode ||
|
|
197
|
+
composition.reconciliation.sourceTotal !== baseline.total.amount)
|
|
198
|
+
return false;
|
|
199
|
+
const baselineComponentById = new Map(baseline.components.map(component => [component.componentId, component]));
|
|
200
|
+
const partitionedComponentIds = composition.buckets.flatMap(bucket => bucket.componentIds);
|
|
201
|
+
if (partitionedComponentIds.length !== baseline.components.length ||
|
|
202
|
+
new Set(partitionedComponentIds).size !== partitionedComponentIds.length ||
|
|
203
|
+
partitionedComponentIds.some(componentId => !baselineComponentById.has(componentId)))
|
|
204
|
+
return false;
|
|
205
|
+
for (const bucket of composition.buckets) {
|
|
206
|
+
try {
|
|
207
|
+
if (formatExactDecimalValue(sumCanonicalDecimals(bucket.componentIds.map(componentId => baselineComponentById.get(componentId).amount))) !== bucket.amount)
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
176
215
|
const financialOwnerBaselines = coordinate.ownerBaselines.filter(baseline => financialRoleByScope.get(baseline.scopeId) === 'owner');
|
|
177
216
|
const availableMembers = [...financialOwnerBaselines, coordinate.residualBaseline]
|
|
178
217
|
.filter(baseline => baseline.status === 'available')
|
|
@@ -256,7 +295,7 @@ const isCoordinate = (value, coveredScopeIds, financialRoleByScope, providerAcco
|
|
|
256
295
|
(projection.targetEvidenceBundleId !== undefined && !bundleIds.has(projection.targetEvidenceBundleId)) ||
|
|
257
296
|
(targetAssessment !== undefined &&
|
|
258
297
|
(targetAssessment.request.scopeId !== projection.scopeId ||
|
|
259
|
-
targetAssessment.result !== 'available' ||
|
|
298
|
+
(projection.status === 'available' && targetAssessment.result !== 'available') ||
|
|
260
299
|
targetAssessment.evidenceBundleId !== projection.targetEvidenceBundleId)) ||
|
|
261
300
|
(projection.baselineId !== undefined && (owner.status !== 'available' || projection.baselineId !== owner.baselineId)) ||
|
|
262
301
|
(projection.status === 'available' && owner.status !== 'available'))
|
|
@@ -328,6 +367,15 @@ const isCoordinate = (value, coveredScopeIds, financialRoleByScope, providerAcco
|
|
|
328
367
|
return false;
|
|
329
368
|
continue;
|
|
330
369
|
}
|
|
370
|
+
if (projection.operationKind === 'replace-quantity-and-rate') {
|
|
371
|
+
if (!('targetQuantity' in applied) || !('targetRate' in applied))
|
|
372
|
+
return false;
|
|
373
|
+
if (applied.targetQuantity.unit !== applied.targetRate.quantityUnit ||
|
|
374
|
+
applied.targetRate.currencyCode !== projection.accountingCurrencyCode ||
|
|
375
|
+
formatExactDecimalValue(multiplyExactDecimalValues(parseCanonicalDecimal(applied.targetQuantity.amount), parseCanonicalDecimal(applied.targetRate.amount))) !== applied.targetAmount)
|
|
376
|
+
return false;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
331
379
|
if (!('sourceRate' in applied) || !('targetQuantity' in applied) || component.effectiveRate === undefined)
|
|
332
380
|
return false;
|
|
333
381
|
if (applied.sourceRate.amount !== component.effectiveRate.amount ||
|
|
@@ -495,3 +543,158 @@ export const isFinancialAuthorityViewV1 = (value) => {
|
|
|
495
543
|
export const isFinancialAuthorityViewBoundToArtifactGenerationV1 = (value, expected) => isFinancialAuthorityViewV1(value) &&
|
|
496
544
|
value.artifactGeneration.runId === expected.runId &&
|
|
497
545
|
value.artifactGeneration.generatedAt === expected.generatedAt;
|
|
546
|
+
const isSameProviderAccountSet = (left, right) => {
|
|
547
|
+
if (left.length !== right.length)
|
|
548
|
+
return false;
|
|
549
|
+
const sortedLeft = [...left].sort();
|
|
550
|
+
const sortedRight = [...right].sort();
|
|
551
|
+
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
552
|
+
};
|
|
553
|
+
const isResourceProjectionCoordinate = (value, projection, assessmentById, bundleIds) => {
|
|
554
|
+
if (!isRecord(value) ||
|
|
555
|
+
!hasExactFields(value, [
|
|
556
|
+
'coordinateId',
|
|
557
|
+
'periodRole',
|
|
558
|
+
'period',
|
|
559
|
+
'costBasis',
|
|
560
|
+
'estimateLens',
|
|
561
|
+
'ownerBaseline',
|
|
562
|
+
'componentDescriptors',
|
|
563
|
+
'displayRollups',
|
|
564
|
+
'projections',
|
|
565
|
+
], ['requestedCurrencyCode', 'chargeComposition']) ||
|
|
566
|
+
!isHash(value.coordinateId) ||
|
|
567
|
+
(value.periodRole !== 'current' && value.periodRole !== 'previous') ||
|
|
568
|
+
!isFinancialBaselinePeriodV2(value.period) ||
|
|
569
|
+
typeof value.costBasis !== 'string' ||
|
|
570
|
+
!COST_BASES.has(value.costBasis) ||
|
|
571
|
+
typeof value.estimateLens !== 'string' ||
|
|
572
|
+
!ESTIMATE_LENSES.has(value.estimateLens) ||
|
|
573
|
+
(value.requestedCurrencyCode !== undefined && (typeof value.requestedCurrencyCode !== 'string' || !CURRENCY.test(value.requestedCurrencyCode))) ||
|
|
574
|
+
!isFinancialScopeBaselineEnvelopeV2(value.ownerBaseline) ||
|
|
575
|
+
(value.chargeComposition !== undefined && !isFinancialChargeCompositionV1(value.chargeComposition)) ||
|
|
576
|
+
!Array.isArray(value.componentDescriptors) ||
|
|
577
|
+
value.componentDescriptors.length > 20000 ||
|
|
578
|
+
!value.componentDescriptors.every(isFinancialAuthorityComponentDescriptorV1) ||
|
|
579
|
+
!Array.isArray(value.displayRollups) ||
|
|
580
|
+
value.displayRollups.length > 20000 ||
|
|
581
|
+
!value.displayRollups.every(isFinancialDisplayRollupV1) ||
|
|
582
|
+
!Array.isArray(value.projections) ||
|
|
583
|
+
value.projections.length > 20000 ||
|
|
584
|
+
!value.projections.every(isFinancialProjectionEnvelopeV1))
|
|
585
|
+
return false;
|
|
586
|
+
const coordinate = value;
|
|
587
|
+
const baseline = coordinate.ownerBaseline;
|
|
588
|
+
const assessment = assessmentById.get(baseline.assessmentId);
|
|
589
|
+
if (normalizeScopeId(baseline.scopeId) !== projection.scopeId ||
|
|
590
|
+
baseline.costBasis !== coordinate.costBasis ||
|
|
591
|
+
baseline.estimateLens !== coordinate.estimateLens ||
|
|
592
|
+
periodText(baseline.period) !== periodText(coordinate.period) ||
|
|
593
|
+
baseline.requestedCurrencyCode !== coordinate.requestedCurrencyCode ||
|
|
594
|
+
!isSameProviderAccountSet(baseline.providerAccountRefs, projection.providerAccountRefs) ||
|
|
595
|
+
!assessment ||
|
|
596
|
+
assessment.request.scopeId !== baseline.scopeId ||
|
|
597
|
+
assessment.request.scopeKind !== baseline.scopeKind ||
|
|
598
|
+
assessment.result !== baseline.status)
|
|
599
|
+
return false;
|
|
600
|
+
if (projection.financialRole === 'display-only' && (baseline.status !== 'unavailable' || baseline.unavailableReason !== 'unsupported-scope')) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
if (projection.financialRole === 'unclassified' && (baseline.status !== 'unavailable' || baseline.unavailableReason !== 'ownership-unresolved')) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if (baseline.status !== 'available' || baseline.baselineKind !== 'owner') {
|
|
607
|
+
return (coordinate.chargeComposition === undefined &&
|
|
608
|
+
coordinate.componentDescriptors.length === 0 &&
|
|
609
|
+
coordinate.displayRollups.length === 0 &&
|
|
610
|
+
coordinate.projections.every(projectionValue => normalizeScopeId(projectionValue.scopeId) === projection.scopeId) &&
|
|
611
|
+
new Set(coordinate.projections.map(projectionValue => projectionValue.scenarioId)).size === coordinate.projections.length);
|
|
612
|
+
}
|
|
613
|
+
if (baseline.scopeKind !== 'canonical-resource-owner' && baseline.scopeKind !== 'composite-resource')
|
|
614
|
+
return false;
|
|
615
|
+
if (!bundleIds.has(baseline.evidenceBundleId) || assessment.evidenceBundleId !== baseline.evidenceBundleId)
|
|
616
|
+
return false;
|
|
617
|
+
const composition = coordinate.chargeComposition;
|
|
618
|
+
if (!composition ||
|
|
619
|
+
composition.baselineId !== baseline.baselineId ||
|
|
620
|
+
normalizeScopeId(composition.ownerScopeId) !== projection.scopeId ||
|
|
621
|
+
periodText(composition.period) !== periodText(baseline.period) ||
|
|
622
|
+
composition.costBasis !== baseline.costBasis ||
|
|
623
|
+
composition.estimateLens !== baseline.estimateLens ||
|
|
624
|
+
composition.accountingCurrencyCode !== baseline.total.currencyCode ||
|
|
625
|
+
composition.reconciliation.sourceTotal !== baseline.total.amount)
|
|
626
|
+
return false;
|
|
627
|
+
const componentKeys = new Set(baseline.components.map(component => `${baseline.baselineId}\u0000${component.componentId}`));
|
|
628
|
+
const descriptorKeys = coordinate.componentDescriptors.map(descriptor => `${descriptor.baselineId}\u0000${descriptor.componentId}`);
|
|
629
|
+
if (descriptorKeys.length !== componentKeys.size ||
|
|
630
|
+
new Set(descriptorKeys).size !== descriptorKeys.length ||
|
|
631
|
+
descriptorKeys.some(key => !componentKeys.has(key)) ||
|
|
632
|
+
coordinate.displayRollups.some(rollup => normalizeScopeId(rollup.displayScopeId) !== projection.scopeId ||
|
|
633
|
+
rollup.members.some(member => !componentKeys.has(`${member.baselineId}\u0000${member.componentId}`))) ||
|
|
634
|
+
coordinate.projections.some(projectionValue => normalizeScopeId(projectionValue.scopeId) !== projection.scopeId))
|
|
635
|
+
return false;
|
|
636
|
+
const rolledUpKeys = coordinate.displayRollups.flatMap(rollup => rollup.members.map(member => `${member.baselineId}\u0000${member.componentId}`));
|
|
637
|
+
return (rolledUpKeys.length === componentKeys.size &&
|
|
638
|
+
new Set(rolledUpKeys).size === rolledUpKeys.length &&
|
|
639
|
+
rolledUpKeys.every(key => componentKeys.has(key)) &&
|
|
640
|
+
new Set(coordinate.projections.map(projectionValue => projectionValue.scenarioId)).size === coordinate.projections.length);
|
|
641
|
+
};
|
|
642
|
+
/** Strictly validates one bounded, non-additive resource projection without requiring the full authority ledger. */
|
|
643
|
+
export const isFinancialAuthorityResourceProjectionV1 = (value) => {
|
|
644
|
+
if (!isRecord(value) ||
|
|
645
|
+
!hasExactFields(value, [
|
|
646
|
+
'contractVersion',
|
|
647
|
+
'authorityId',
|
|
648
|
+
'provider',
|
|
649
|
+
'providerAccountRefs',
|
|
650
|
+
'artifactGeneration',
|
|
651
|
+
'billingGenerationId',
|
|
652
|
+
'resourceType',
|
|
653
|
+
'financialRole',
|
|
654
|
+
'scopeId',
|
|
655
|
+
'evidenceBundles',
|
|
656
|
+
'evidenceAssessments',
|
|
657
|
+
'coordinates',
|
|
658
|
+
]) ||
|
|
659
|
+
value.contractVersion !== 'financial-authority-resource-projection/v1' ||
|
|
660
|
+
!isHash(value.authorityId) ||
|
|
661
|
+
value.provider !== 'azure' ||
|
|
662
|
+
!Array.isArray(value.providerAccountRefs) ||
|
|
663
|
+
value.providerAccountRefs.length === 0 ||
|
|
664
|
+
value.providerAccountRefs.length > 64 ||
|
|
665
|
+
!value.providerAccountRefs.every(isIdentity) ||
|
|
666
|
+
new Set(value.providerAccountRefs).size !== value.providerAccountRefs.length ||
|
|
667
|
+
!isRecord(value.artifactGeneration) ||
|
|
668
|
+
!hasExactFields(value.artifactGeneration, ['runId', 'generatedAt']) ||
|
|
669
|
+
!isIdentity(value.artifactGeneration.runId) ||
|
|
670
|
+
!isIsoInstant(value.artifactGeneration.generatedAt) ||
|
|
671
|
+
!isIdentity(value.billingGenerationId) ||
|
|
672
|
+
typeof value.resourceType !== 'string' ||
|
|
673
|
+
!AZURE_RESOURCE_TYPE.test(value.resourceType) ||
|
|
674
|
+
value.resourceType !== normalizeScopeId(value.resourceType) ||
|
|
675
|
+
(value.financialRole !== 'owner' && value.financialRole !== 'display-only' && value.financialRole !== 'unclassified') ||
|
|
676
|
+
!isIdentity(value.scopeId) ||
|
|
677
|
+
value.scopeId !== normalizeScopeId(value.scopeId) ||
|
|
678
|
+
!Array.isArray(value.evidenceBundles) ||
|
|
679
|
+
value.evidenceBundles.length > 20000 ||
|
|
680
|
+
!value.evidenceBundles.every(isFinancialEvidenceBundleV1) ||
|
|
681
|
+
!Array.isArray(value.evidenceAssessments) ||
|
|
682
|
+
value.evidenceAssessments.length > 20000 ||
|
|
683
|
+
!value.evidenceAssessments.every(isFinancialEvidenceAssessmentV1) ||
|
|
684
|
+
!Array.isArray(value.coordinates) ||
|
|
685
|
+
value.coordinates.length === 0 ||
|
|
686
|
+
value.coordinates.length > 128)
|
|
687
|
+
return false;
|
|
688
|
+
const projection = value;
|
|
689
|
+
const bundleIds = new Set(projection.evidenceBundles.map(bundle => bundle.bundleId));
|
|
690
|
+
const assessmentById = new Map(projection.evidenceAssessments.map(assessment => [assessment.assessmentId, assessment]));
|
|
691
|
+
if (bundleIds.size !== projection.evidenceBundles.length || assessmentById.size !== projection.evidenceAssessments.length)
|
|
692
|
+
return false;
|
|
693
|
+
if (projection.evidenceAssessments.some(assessment => (assessment.evidenceBundleId !== undefined && !bundleIds.has(assessment.evidenceBundleId)) ||
|
|
694
|
+
!isSameProviderAccountSet(assessment.request.providerAccountRefs, projection.providerAccountRefs) ||
|
|
695
|
+
normalizeScopeId(assessment.request.scopeId) !== projection.scopeId) ||
|
|
696
|
+
!projection.coordinates.every(coordinate => isResourceProjectionCoordinate(coordinate, projection, assessmentById, bundleIds)) ||
|
|
697
|
+
new Set(projection.coordinates.map(coordinate => coordinate.coordinateId)).size !== projection.coordinates.length)
|
|
698
|
+
return false;
|
|
699
|
+
return true;
|
|
700
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { sha256Utf8 } from '../common/sha256.js';
|
|
2
|
+
export const FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1 = 1;
|
|
3
|
+
export const FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1 = 'financial-charge-composition/v1';
|
|
4
|
+
export const FINANCIAL_CHARGE_INCLUSION_POLICY_CONTRACT_VERSION_V1 = 'financial-charge-inclusion-policy/v1';
|
|
5
|
+
const createRegisteredPolicy = (definition) => {
|
|
6
|
+
const { policyId, ...policy } = definition;
|
|
7
|
+
const registered = {
|
|
8
|
+
...policy,
|
|
9
|
+
includeSources: Object.freeze([...policy.includeSources]),
|
|
10
|
+
excludeSources: Object.freeze([...policy.excludeSources]),
|
|
11
|
+
withholdSources: Object.freeze([...policy.withholdSources]),
|
|
12
|
+
policyRef: Object.freeze({ policyId, policyDigest: `sha256:${sha256Utf8(JSON.stringify(definition))}` }),
|
|
13
|
+
};
|
|
14
|
+
return Object.freeze(registered);
|
|
15
|
+
};
|
|
16
|
+
export const AZURE_BILLED_ALL_CHARGES_POLICY_V1 = createRegisteredPolicy({
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
contractVersion: FINANCIAL_CHARGE_INCLUSION_POLICY_CONTRACT_VERSION_V1,
|
|
19
|
+
policyId: 'azure-billed-all-charges/v1',
|
|
20
|
+
includeSources: ['azure-native', 'marketplace', 'unknown'],
|
|
21
|
+
excludeSources: [],
|
|
22
|
+
withholdSources: [],
|
|
23
|
+
});
|
|
24
|
+
export const AZURE_CLOUD_SERVICES_EXCLUDING_MARKETPLACE_POLICY_V1 = createRegisteredPolicy({
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
contractVersion: FINANCIAL_CHARGE_INCLUSION_POLICY_CONTRACT_VERSION_V1,
|
|
27
|
+
policyId: 'azure-cloud-services-excluding-marketplace/v1',
|
|
28
|
+
includeSources: ['azure-native'],
|
|
29
|
+
excludeSources: ['marketplace'],
|
|
30
|
+
withholdSources: ['unknown'],
|
|
31
|
+
});
|
|
32
|
+
export const FINANCIAL_CHARGE_INCLUSION_POLICIES_V1 = Object.freeze([
|
|
33
|
+
AZURE_BILLED_ALL_CHARGES_POLICY_V1,
|
|
34
|
+
AZURE_CLOUD_SERVICES_EXCLUDING_MARKETPLACE_POLICY_V1,
|
|
35
|
+
]);
|
|
36
|
+
export const resolveFinancialChargeInclusionPolicyV1 = (policyRef) => policyRef !== null && typeof policyRef === 'object' && FINANCIAL_CHARGE_INCLUSION_POLICIES_V1.find(policy => policy.policyRef.policyId === policyRef.policyId && policy.policyRef.policyDigest === policyRef.policyDigest) || undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatExactDecimalValue, sumCanonicalDecimals } from '../common/exactDecimal.js';
|
|
2
|
+
import { FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1, } from './financialChargeComposition.js';
|
|
3
|
+
import { createFinancialChargeCompositionIdV1 } from './financialChargeCompositionValidation.js';
|
|
4
|
+
const bucketKey = (component) => `${component.chargeSource}\u0000${component.chargeRecurrence}\u0000${component.chargeClassification}`;
|
|
5
|
+
export const createFinancialChargeCompositionV1 = (request) => {
|
|
6
|
+
if (!Array.isArray(request.components) || request.components.length === 0 || request.components.length > 20000) {
|
|
7
|
+
throw new TypeError('Financial charge composition requires a bounded non-empty component collection.');
|
|
8
|
+
}
|
|
9
|
+
if (new Set(request.components.map(component => component.componentId)).size !== request.components.length) {
|
|
10
|
+
throw new TypeError('Financial charge composition cannot classify one component more than once.');
|
|
11
|
+
}
|
|
12
|
+
const groups = new Map();
|
|
13
|
+
for (const component of request.components) {
|
|
14
|
+
const key = bucketKey(component);
|
|
15
|
+
const group = groups.get(key);
|
|
16
|
+
if (group)
|
|
17
|
+
group.push(component);
|
|
18
|
+
else
|
|
19
|
+
groups.set(key, [component]);
|
|
20
|
+
}
|
|
21
|
+
const buckets = [...groups.values()]
|
|
22
|
+
.map(group => ({
|
|
23
|
+
chargeSource: group[0].chargeSource,
|
|
24
|
+
chargeRecurrence: group[0].chargeRecurrence,
|
|
25
|
+
chargeClassification: group[0].chargeClassification,
|
|
26
|
+
amount: formatExactDecimalValue(sumCanonicalDecimals(group.map(component => component.amount))),
|
|
27
|
+
componentIds: [...new Set(group.map(component => component.componentId))].sort(),
|
|
28
|
+
evidenceRefIds: [...new Set(group.flatMap(component => component.evidenceRefIds))].sort(),
|
|
29
|
+
}))
|
|
30
|
+
.sort((left, right) => `${left.chargeSource}\u0000${left.chargeRecurrence}\u0000${left.chargeClassification}`.localeCompare(`${right.chargeSource}\u0000${right.chargeRecurrence}\u0000${right.chargeClassification}`));
|
|
31
|
+
const bucketTotal = formatExactDecimalValue(sumCanonicalDecimals(buckets.map(bucket => bucket.amount)));
|
|
32
|
+
if (bucketTotal !== request.sourceTotal)
|
|
33
|
+
throw new TypeError('Financial charge composition does not reconcile to its source total.');
|
|
34
|
+
const identity = {
|
|
35
|
+
schemaVersion: FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1,
|
|
36
|
+
contractVersion: FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1,
|
|
37
|
+
baselineId: request.baselineId,
|
|
38
|
+
ownerScopeId: request.ownerScopeId,
|
|
39
|
+
period: request.period,
|
|
40
|
+
costBasis: request.costBasis,
|
|
41
|
+
estimateLens: request.estimateLens,
|
|
42
|
+
accountingCurrencyCode: request.accountingCurrencyCode,
|
|
43
|
+
buckets,
|
|
44
|
+
reconciliation: { status: 'reconciled', bucketTotal, sourceTotal: request.sourceTotal, difference: '0' },
|
|
45
|
+
algorithmVersion: request.algorithmVersion,
|
|
46
|
+
};
|
|
47
|
+
return { ...identity, chargeCompositionId: createFinancialChargeCompositionIdV1(identity) };
|
|
48
|
+
};
|