@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.
Files changed (80) hide show
  1. package/dist/azure/financialAnalytics.d.ts +68 -2
  2. package/dist/azure/financialAnalytics.d.ts.map +1 -1
  3. package/dist/azure/financialAnalyticsDelivery.d.ts +2 -0
  4. package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
  5. package/dist/azure/financialAnalyticsDelivery.js +4 -1
  6. package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
  7. package/dist/azure/financialAnalyticsDisplayKernel.d.ts +12 -0
  8. package/dist/azure/financialAnalyticsDisplayKernel.d.ts.map +1 -0
  9. package/dist/azure/financialAnalyticsDisplayKernel.js +144 -0
  10. package/dist/azure/financialAnalyticsDisplayKernel.js.map +1 -0
  11. package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
  12. package/dist/azure/financialAnalyticsValidation.js +61 -7
  13. package/dist/azure/financialAnalyticsValidation.js.map +1 -1
  14. package/dist/azure/financialAuthorityView.d.ts +4 -0
  15. package/dist/azure/financialAuthorityView.d.ts.map +1 -1
  16. package/dist/azure/financialAuthorityView.js.map +1 -1
  17. package/dist/azure/financialAuthorityViewValidation.d.ts.map +1 -1
  18. package/dist/azure/financialAuthorityViewValidation.js +39 -0
  19. package/dist/azure/financialAuthorityViewValidation.js.map +1 -1
  20. package/dist/azure/financialChargeComposition.d.ts +81 -0
  21. package/dist/azure/financialChargeComposition.d.ts.map +1 -0
  22. package/dist/azure/financialChargeComposition.js +41 -0
  23. package/dist/azure/financialChargeComposition.js.map +1 -0
  24. package/dist/azure/financialChargeCompositionKernel.d.ts +3 -0
  25. package/dist/azure/financialChargeCompositionKernel.d.ts.map +1 -0
  26. package/dist/azure/financialChargeCompositionKernel.js +49 -0
  27. package/dist/azure/financialChargeCompositionKernel.js.map +1 -0
  28. package/dist/azure/financialChargeCompositionValidation.d.ts +8 -0
  29. package/dist/azure/financialChargeCompositionValidation.d.ts.map +1 -0
  30. package/dist/azure/financialChargeCompositionValidation.js +164 -0
  31. package/dist/azure/financialChargeCompositionValidation.js.map +1 -0
  32. package/dist/azure/financialDataflow.d.ts +8 -1
  33. package/dist/azure/financialDataflow.d.ts.map +1 -1
  34. package/dist/azure/financialDataflow.js.map +1 -1
  35. package/dist/azure/financialDataflowKernel.d.ts +2 -0
  36. package/dist/azure/financialDataflowKernel.d.ts.map +1 -1
  37. package/dist/azure/financialDataflowKernel.js +59 -7
  38. package/dist/azure/financialDataflowKernel.js.map +1 -1
  39. package/dist/azure/financialDataflowValidation.d.ts +1 -1
  40. package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
  41. package/dist/azure/financialDataflowValidation.js +146 -28
  42. package/dist/azure/financialDataflowValidation.js.map +1 -1
  43. package/dist/azure/financialPolicy.d.ts +3 -0
  44. package/dist/azure/financialPolicy.d.ts.map +1 -1
  45. package/dist/azure/financialPolicy.js.map +1 -1
  46. package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
  47. package/dist/azure/financialPolicyValidation.js +5 -0
  48. package/dist/azure/financialPolicyValidation.js.map +1 -1
  49. package/dist/azure/financialScope.d.ts +4 -0
  50. package/dist/azure/financialScope.d.ts.map +1 -1
  51. package/dist/azure/financialScope.js +4 -0
  52. package/dist/azure/financialScope.js.map +1 -1
  53. package/dist/azure/metrics.d.ts +2 -2
  54. package/dist/azure/metrics.d.ts.map +1 -1
  55. package/dist/azure/prices.d.ts +16 -3
  56. package/dist/azure/prices.d.ts.map +1 -1
  57. package/dist/azure/recommendations.d.ts +12 -4
  58. package/dist/azure/recommendations.d.ts.map +1 -1
  59. package/dist/azure/reports.d.ts +3 -2
  60. package/dist/azure/reports.d.ts.map +1 -1
  61. package/dist/azure/reports.js.map +1 -1
  62. package/dist/azure/resources.d.ts +12 -0
  63. package/dist/azure/resources.d.ts.map +1 -1
  64. package/dist/azure/subscriptions.d.ts +4 -2
  65. package/dist/azure/subscriptions.d.ts.map +1 -1
  66. package/dist/azure/views.d.ts +7 -2
  67. package/dist/azure/views.d.ts.map +1 -1
  68. package/dist/azure/views.js.map +1 -1
  69. package/dist/esm/azure/financialAnalyticsDelivery.js +2 -0
  70. package/dist/esm/azure/financialAnalyticsDisplayKernel.js +139 -0
  71. package/dist/esm/azure/financialAnalyticsValidation.js +61 -7
  72. package/dist/esm/azure/financialAuthorityViewValidation.js +39 -0
  73. package/dist/esm/azure/financialChargeComposition.js +36 -0
  74. package/dist/esm/azure/financialChargeCompositionKernel.js +44 -0
  75. package/dist/esm/azure/financialChargeCompositionValidation.js +155 -0
  76. package/dist/esm/azure/financialDataflowKernel.js +59 -7
  77. package/dist/esm/azure/financialDataflowValidation.js +146 -28
  78. package/dist/esm/azure/financialPolicyValidation.js +5 -0
  79. package/dist/esm/azure/financialScope.js +4 -0
  80. 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 members = sortedBaselines.map(baseline => baseline.status === 'available'
66
- ? { memberScopeId: baseline.scopeId, baselineId: baseline.baselineId, status: 'included' }
67
- : { memberScopeId: baseline.scopeId, status: 'unavailable', reasonCode: baseline.unavailableReason });
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 || available.length === 0) {
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 knownAmount = formatExactDecimalValue(sumCanonicalDecimals(available.map(baseline => baseline.total.amount)));
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;