@spottoai/types-package 1.0.2-dev1138.2 → 1.0.2-dev1138.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/azure/financialAnalytics.d.ts +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/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.map +1 -1
- package/dist/azure/financialAuthorityViewValidation.js +39 -0
- 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 +49 -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 +59 -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/financialScope.d.ts +4 -0
- package/dist/azure/financialScope.d.ts.map +1 -1
- package/dist/azure/financialScope.js +4 -0
- package/dist/azure/financialScope.js.map +1 -1
- package/dist/azure/metrics.d.ts +2 -2
- package/dist/azure/metrics.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/recommendations.d.ts +12 -4
- 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/subscriptions.d.ts +4 -2
- package/dist/azure/subscriptions.d.ts.map +1 -1
- package/dist/azure/views.d.ts +7 -2
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js.map +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/financialAuthorityViewValidation.js +39 -0
- package/dist/esm/azure/financialChargeComposition.js +36 -0
- package/dist/esm/azure/financialChargeCompositionKernel.js +44 -0
- package/dist/esm/azure/financialChargeCompositionValidation.js +155 -0
- package/dist/esm/azure/financialDataflowKernel.js +59 -7
- package/dist/esm/azure/financialDataflowValidation.js +146 -28
- package/dist/esm/azure/financialPolicyValidation.js +5 -0
- package/dist/esm/azure/financialScope.js +4 -0
- package/package.json +3 -2
|
@@ -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')
|
|
@@ -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,44 @@
|
|
|
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
|
+
groups.set(key, [...(groups.get(key) ?? []), component]);
|
|
16
|
+
}
|
|
17
|
+
const buckets = [...groups.values()]
|
|
18
|
+
.map(group => ({
|
|
19
|
+
chargeSource: group[0].chargeSource,
|
|
20
|
+
chargeRecurrence: group[0].chargeRecurrence,
|
|
21
|
+
chargeClassification: group[0].chargeClassification,
|
|
22
|
+
amount: formatExactDecimalValue(sumCanonicalDecimals(group.map(component => component.amount))),
|
|
23
|
+
componentIds: [...new Set(group.map(component => component.componentId))].sort(),
|
|
24
|
+
evidenceRefIds: [...new Set(group.flatMap(component => component.evidenceRefIds))].sort(),
|
|
25
|
+
}))
|
|
26
|
+
.sort((left, right) => `${left.chargeSource}\u0000${left.chargeRecurrence}\u0000${left.chargeClassification}`.localeCompare(`${right.chargeSource}\u0000${right.chargeRecurrence}\u0000${right.chargeClassification}`));
|
|
27
|
+
const bucketTotal = formatExactDecimalValue(sumCanonicalDecimals(buckets.map(bucket => bucket.amount)));
|
|
28
|
+
if (bucketTotal !== request.sourceTotal)
|
|
29
|
+
throw new TypeError('Financial charge composition does not reconcile to its source total.');
|
|
30
|
+
const identity = {
|
|
31
|
+
schemaVersion: FINANCIAL_CHARGE_COMPOSITION_SCHEMA_VERSION_V1,
|
|
32
|
+
contractVersion: FINANCIAL_CHARGE_COMPOSITION_CONTRACT_VERSION_V1,
|
|
33
|
+
baselineId: request.baselineId,
|
|
34
|
+
ownerScopeId: request.ownerScopeId,
|
|
35
|
+
period: request.period,
|
|
36
|
+
costBasis: request.costBasis,
|
|
37
|
+
estimateLens: request.estimateLens,
|
|
38
|
+
accountingCurrencyCode: request.accountingCurrencyCode,
|
|
39
|
+
buckets,
|
|
40
|
+
reconciliation: { status: 'reconciled', bucketTotal, sourceTotal: request.sourceTotal, difference: '0' },
|
|
41
|
+
algorithmVersion: request.algorithmVersion,
|
|
42
|
+
};
|
|
43
|
+
return { ...identity, chargeCompositionId: createFinancialChargeCompositionIdV1(identity) };
|
|
44
|
+
};
|
|
@@ -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,49 @@ 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 includedBaselines = available.filter(baseline => members.some(member => member.status === 'included' && member.baselineId === baseline.baselineId));
|
|
69
96
|
const unavailableReasons = new Set(sortedBaselines
|
|
70
97
|
.filter((baseline) => baseline.status === 'unavailable')
|
|
71
98
|
.map(baseline => baseline.unavailableReason));
|
|
99
|
+
members.filter(member => member.status === 'unavailable').forEach(member => unavailableReasons.add(member.reasonCode));
|
|
100
|
+
const selectionReasonCodes = new Set();
|
|
101
|
+
selections.forEach(selection => selection.reasonCodes?.forEach(reason => {
|
|
102
|
+
selectionReasonCodes.add(reason);
|
|
103
|
+
unavailableReasons.add(reason);
|
|
104
|
+
}));
|
|
72
105
|
if (available.some(baseline => !isCompleteFinancialBaselinePeriodV2(baseline.period)))
|
|
73
106
|
unavailableReasons.add('coverage-incomplete');
|
|
74
107
|
if (request.coordinate.accountingCurrency.status === 'unresolved') {
|
|
@@ -84,7 +117,7 @@ export const composeCurrentSpendV1 = (request) => {
|
|
|
84
117
|
};
|
|
85
118
|
const currency = request.coordinate.accountingCurrency.status === 'resolved' ? request.coordinate.accountingCurrency.currencyCode : undefined;
|
|
86
119
|
let identity;
|
|
87
|
-
if (!currency ||
|
|
120
|
+
if (!currency || includedBaselines.length === 0) {
|
|
88
121
|
identity = {
|
|
89
122
|
...common,
|
|
90
123
|
amount: {
|
|
@@ -94,17 +127,36 @@ export const composeCurrentSpendV1 = (request) => {
|
|
|
94
127
|
};
|
|
95
128
|
}
|
|
96
129
|
else {
|
|
97
|
-
const
|
|
130
|
+
const includedSelections = members.flatMap(member => member.status === 'included' ? [selections.get(member.baselineId)] : []);
|
|
131
|
+
const aggregateSelection = {
|
|
132
|
+
status: includedSelections.some(selection => selection.status === 'partial') ? 'partial' : 'available',
|
|
133
|
+
includedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.includedAmount))),
|
|
134
|
+
excludedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.excludedAmount))),
|
|
135
|
+
withheldAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.withheldAmount))),
|
|
136
|
+
forecastEligibleAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.forecastEligibleAmount))),
|
|
137
|
+
oneTimeAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.oneTimeAmount))),
|
|
138
|
+
unknownRecurrenceAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.unknownRecurrenceAmount))),
|
|
139
|
+
forecastStatus: includedSelections.some(selection => selection.forecastStatus === 'partial') ? 'partial' : 'available',
|
|
140
|
+
currencyCode: currency,
|
|
141
|
+
...(selectionReasonCodes.size === 0 ? {} : { reasonCodes: uniqueSorted(selectionReasonCodes) }),
|
|
142
|
+
...(includedSelections.every(selection => selection.forecastReasonCodes === undefined)
|
|
143
|
+
? {}
|
|
144
|
+
: {
|
|
145
|
+
forecastReasonCodes: uniqueSorted(includedSelections.flatMap(selection => selection.forecastReasonCodes ?? [])),
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
const knownAmount = aggregateSelection.includedAmount;
|
|
98
149
|
identity =
|
|
99
150
|
unavailableReasons.size === 0
|
|
100
|
-
? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency } }
|
|
151
|
+
? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency }, chargeSelection: aggregateSelection }
|
|
101
152
|
: {
|
|
102
153
|
...common,
|
|
103
154
|
amount: { status: 'partial', knownAmount, currencyCode: currency, reasonCodes: uniqueSorted(unavailableReasons) },
|
|
155
|
+
chargeSelection: aggregateSelection,
|
|
104
156
|
};
|
|
105
157
|
}
|
|
106
158
|
const composition = { ...identity, compositionId: createCurrentSpendCompositionIdV1(identity) };
|
|
107
|
-
if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines)) {
|
|
159
|
+
if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines, request.chargeCompositions)) {
|
|
108
160
|
throw new TypeError('Current-spend composition does not reconcile to its authoritative baseline members.');
|
|
109
161
|
}
|
|
110
162
|
return composition;
|