@spottoai/types-package 1.0.2-dev1138.1 → 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 +80 -8
- package/dist/azure/financialAnalytics.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDelivery.d.ts +72 -3
- package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDelivery.js +7 -1
- package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.d.ts +9 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.d.ts.map +1 -1
- package/dist/azure/financialAnalyticsDeliveryValidation.js +190 -10
- package/dist/azure/financialAnalyticsDeliveryValidation.js.map +1 -1
- package/dist/azure/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 +101 -19
- 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 +45 -2
- package/dist/azure/financialDataflow.d.ts.map +1 -1
- package/dist/azure/financialDataflow.js.map +1 -1
- package/dist/azure/financialDataflowKernel.d.ts +31 -0
- package/dist/azure/financialDataflowKernel.d.ts.map +1 -0
- package/dist/azure/financialDataflowKernel.js +169 -0
- package/dist/azure/financialDataflowKernel.js.map +1 -0
- package/dist/azure/financialDataflowValidation.d.ts +5 -2
- package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
- package/dist/azure/financialDataflowValidation.js +203 -30
- package/dist/azure/financialDataflowValidation.js.map +1 -1
- package/dist/azure/financialEvidenceAssessmentValidation.d.ts.map +1 -1
- package/dist/azure/financialEvidenceAssessmentValidation.js +7 -0
- package/dist/azure/financialEvidenceAssessmentValidation.js.map +1 -1
- package/dist/azure/financialPolicy.d.ts +19 -3
- package/dist/azure/financialPolicy.d.ts.map +1 -1
- package/dist/azure/financialPolicy.js.map +1 -1
- package/dist/azure/financialPolicyDelivery.d.ts +75 -1
- package/dist/azure/financialPolicyDelivery.d.ts.map +1 -1
- package/dist/azure/financialPolicyDelivery.js +5 -1
- package/dist/azure/financialPolicyDelivery.js.map +1 -1
- package/dist/azure/financialPolicyDeliveryValidation.d.ts +16 -2
- package/dist/azure/financialPolicyDeliveryValidation.d.ts.map +1 -1
- package/dist/azure/financialPolicyDeliveryValidation.js +238 -5
- package/dist/azure/financialPolicyDeliveryValidation.js.map +1 -1
- package/dist/azure/financialPolicyKernel.d.ts +38 -0
- package/dist/azure/financialPolicyKernel.d.ts.map +1 -0
- package/dist/azure/financialPolicyKernel.js +232 -0
- package/dist/azure/financialPolicyKernel.js.map +1 -0
- package/dist/azure/financialPolicyValidation.d.ts +1 -0
- package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
- package/dist/azure/financialPolicyValidation.js +111 -16
- package/dist/azure/financialPolicyValidation.js.map +1 -1
- package/dist/azure/financialSavingsAuthority.d.ts +29 -0
- package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthority.js +2 -1
- package/dist/azure/financialSavingsAuthority.js.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsAuthorityValidation.js +3 -0
- package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
- package/dist/azure/financialSavingsCoordinateValidation.js +36 -0
- package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
- package/dist/azure/financialScope.d.ts +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/financialScopeBaseline.d.ts +8 -2
- package/dist/azure/financialScopeBaseline.d.ts.map +1 -1
- package/dist/azure/financialScopeBaseline.js +14 -1
- package/dist/azure/financialScopeBaseline.js.map +1 -1
- package/dist/azure/financialScopeBaselineValidation.d.ts.map +1 -1
- package/dist/azure/financialScopeBaselineValidation.js +15 -1
- package/dist/azure/financialScopeBaselineValidation.js.map +1 -1
- package/dist/azure/financialScopeEvidence.d.ts +1 -1
- package/dist/azure/financialScopeEvidence.d.ts.map +1 -1
- package/dist/azure/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 +21 -5
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js.map +1 -1
- package/dist/esm/azure/financialAnalyticsDelivery.js +5 -0
- package/dist/esm/azure/financialAnalyticsDeliveryValidation.js +184 -11
- package/dist/esm/azure/financialAnalyticsDisplayKernel.js +139 -0
- package/dist/esm/azure/financialAnalyticsValidation.js +101 -19
- 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 +163 -0
- package/dist/esm/azure/financialDataflowValidation.js +199 -29
- package/dist/esm/azure/financialEvidenceAssessmentValidation.js +7 -0
- package/dist/esm/azure/financialPolicyDelivery.js +4 -0
- package/dist/esm/azure/financialPolicyDeliveryValidation.js +227 -7
- package/dist/esm/azure/financialPolicyKernel.js +225 -0
- package/dist/esm/azure/financialPolicyValidation.js +110 -16
- package/dist/esm/azure/financialSavingsAuthority.js +1 -0
- package/dist/esm/azure/financialSavingsAuthorityValidation.js +3 -0
- package/dist/esm/azure/financialSavingsCoordinateValidation.js +36 -0
- package/dist/esm/azure/financialScope.js +6 -0
- package/dist/esm/azure/financialScopeBaseline.js +12 -0
- package/dist/esm/azure/financialScopeBaselineValidation.js +15 -1
- package/dist/events/cost.d.ts +4 -0
- package/dist/events/cost.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { formatExactDecimalValue, parseCanonicalDecimal, subtractExactDecimalValues, sumCanonicalDecimals, } from '../common/exactDecimal.js';
|
|
2
|
+
import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
|
|
3
|
+
import { selectFinancialChargesV1 } from './financialChargeCompositionValidation.js';
|
|
4
|
+
import { createCurrentSpendCompositionIdV1, createCurrentSpendMembershipDigestV1, isCurrentSpendCompositionCompatibleV1, } from './financialDataflowValidation.js';
|
|
5
|
+
import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
|
|
6
|
+
export const CURRENT_SPEND_COMPOSITION_ALGORITHM_VERSION_V1 = 'current-spend-composition/shared-v1';
|
|
7
|
+
const exactPercentAtSixDecimalPlaces = (change, comparison) => {
|
|
8
|
+
if (comparison.coefficient === 0n)
|
|
9
|
+
return undefined;
|
|
10
|
+
const numerator = change.coefficient * 100n * 10n ** BigInt(comparison.scale);
|
|
11
|
+
const denominator = comparison.coefficient * 10n ** BigInt(change.scale);
|
|
12
|
+
for (let scale = 0; scale <= 6; scale += 1) {
|
|
13
|
+
const scaledNumerator = numerator * 10n ** BigInt(scale);
|
|
14
|
+
if (scaledNumerator % denominator === 0n) {
|
|
15
|
+
return formatExactDecimalValue({ coefficient: scaledNumerator / denominator, scale });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Shared exact current-versus-comparison semantics for display composition.
|
|
22
|
+
* Canonical analytics projections use the same direction/change/percentage
|
|
23
|
+
* rules; this helper does not create or impersonate an analytics authority.
|
|
24
|
+
*/
|
|
25
|
+
export const compareFinancialExactAmountsV1 = (request) => {
|
|
26
|
+
if (request.currentAmount === undefined || request.comparisonAmount === undefined)
|
|
27
|
+
return { status: 'unavailable' };
|
|
28
|
+
try {
|
|
29
|
+
const current = parseCanonicalDecimal(request.currentAmount);
|
|
30
|
+
const comparison = parseCanonicalDecimal(request.comparisonAmount);
|
|
31
|
+
if (formatExactDecimalValue(current) !== request.currentAmount ||
|
|
32
|
+
formatExactDecimalValue(comparison) !== request.comparisonAmount) {
|
|
33
|
+
return { status: 'unavailable' };
|
|
34
|
+
}
|
|
35
|
+
const change = subtractExactDecimalValues(current, comparison);
|
|
36
|
+
const direction = change.coefficient === 0n ? 'flat' : change.coefficient > 0n ? 'increasing' : 'decreasing';
|
|
37
|
+
const percentChange = exactPercentAtSixDecimalPlaces(change, comparison);
|
|
38
|
+
return {
|
|
39
|
+
status: 'available',
|
|
40
|
+
direction,
|
|
41
|
+
changeAmount: formatExactDecimalValue(change),
|
|
42
|
+
...(percentChange === undefined ? {} : { percentChange }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { status: 'unavailable' };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const uniqueSorted = (values) => {
|
|
50
|
+
const result = Array.from(new Set(values)).sort();
|
|
51
|
+
if (result.length === 0)
|
|
52
|
+
throw new TypeError('Current-spend unavailable state requires at least one reason code.');
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
/** Portable exact-decimal composition kernel shared by Cloud and browser runtimes. */
|
|
56
|
+
export const composeCurrentSpendV1 = (request) => {
|
|
57
|
+
if (request.coordinate.periodRole !== 'current-spend' && request.coordinate.periodRole !== 'comparison') {
|
|
58
|
+
throw new TypeError('Current-spend composition requires a current-spend or comparison coordinate.');
|
|
59
|
+
}
|
|
60
|
+
if (request.baselines.length === 0)
|
|
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
|
+
}
|
|
69
|
+
const sortedBaselines = [...request.baselines].sort((left, right) => left.scopeId.localeCompare(right.scopeId));
|
|
70
|
+
if (new Set(sortedBaselines.map(baseline => baseline.scopeId)).size !== sortedBaselines.length) {
|
|
71
|
+
throw new TypeError('Current-spend composition cannot contain duplicate member scopes.');
|
|
72
|
+
}
|
|
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
|
+
});
|
|
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));
|
|
96
|
+
const unavailableReasons = new Set(sortedBaselines
|
|
97
|
+
.filter((baseline) => baseline.status === 'unavailable')
|
|
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
|
+
}));
|
|
105
|
+
if (available.some(baseline => !isCompleteFinancialBaselinePeriodV2(baseline.period)))
|
|
106
|
+
unavailableReasons.add('coverage-incomplete');
|
|
107
|
+
if (request.coordinate.accountingCurrency.status === 'unresolved') {
|
|
108
|
+
unavailableReasons.add(request.coordinate.accountingCurrency.reasonCode);
|
|
109
|
+
}
|
|
110
|
+
const common = {
|
|
111
|
+
schemaVersion: FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1,
|
|
112
|
+
contractVersion: FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1,
|
|
113
|
+
coordinate: request.coordinate,
|
|
114
|
+
members,
|
|
115
|
+
membershipDigest: createCurrentSpendMembershipDigestV1(members),
|
|
116
|
+
algorithmVersion: request.algorithmVersion ?? CURRENT_SPEND_COMPOSITION_ALGORITHM_VERSION_V1,
|
|
117
|
+
};
|
|
118
|
+
const currency = request.coordinate.accountingCurrency.status === 'resolved' ? request.coordinate.accountingCurrency.currencyCode : undefined;
|
|
119
|
+
let identity;
|
|
120
|
+
if (!currency || includedBaselines.length === 0) {
|
|
121
|
+
identity = {
|
|
122
|
+
...common,
|
|
123
|
+
amount: {
|
|
124
|
+
status: 'unavailable',
|
|
125
|
+
reasonCodes: uniqueSorted(unavailableReasons.size > 0 ? unavailableReasons : ['evidence-unavailable']),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
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;
|
|
149
|
+
identity =
|
|
150
|
+
unavailableReasons.size === 0
|
|
151
|
+
? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency }, chargeSelection: aggregateSelection }
|
|
152
|
+
: {
|
|
153
|
+
...common,
|
|
154
|
+
amount: { status: 'partial', knownAmount, currencyCode: currency, reasonCodes: uniqueSorted(unavailableReasons) },
|
|
155
|
+
chargeSelection: aggregateSelection,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const composition = { ...identity, compositionId: createCurrentSpendCompositionIdV1(identity) };
|
|
159
|
+
if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines, request.chargeCompositions)) {
|
|
160
|
+
throw new TypeError('Current-spend composition does not reconcile to its authoritative baseline members.');
|
|
161
|
+
}
|
|
162
|
+
return composition;
|
|
163
|
+
};
|