@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.
Files changed (131) hide show
  1. package/dist/azure/financialAnalytics.d.ts +80 -8
  2. package/dist/azure/financialAnalytics.d.ts.map +1 -1
  3. package/dist/azure/financialAnalyticsDelivery.d.ts +72 -3
  4. package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
  5. package/dist/azure/financialAnalyticsDelivery.js +7 -1
  6. package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
  7. package/dist/azure/financialAnalyticsDeliveryValidation.d.ts +9 -1
  8. package/dist/azure/financialAnalyticsDeliveryValidation.d.ts.map +1 -1
  9. package/dist/azure/financialAnalyticsDeliveryValidation.js +190 -10
  10. package/dist/azure/financialAnalyticsDeliveryValidation.js.map +1 -1
  11. package/dist/azure/financialAnalyticsDisplayKernel.d.ts +12 -0
  12. package/dist/azure/financialAnalyticsDisplayKernel.d.ts.map +1 -0
  13. package/dist/azure/financialAnalyticsDisplayKernel.js +144 -0
  14. package/dist/azure/financialAnalyticsDisplayKernel.js.map +1 -0
  15. package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
  16. package/dist/azure/financialAnalyticsValidation.js +101 -19
  17. package/dist/azure/financialAnalyticsValidation.js.map +1 -1
  18. package/dist/azure/financialAuthorityView.d.ts +4 -0
  19. package/dist/azure/financialAuthorityView.d.ts.map +1 -1
  20. package/dist/azure/financialAuthorityView.js.map +1 -1
  21. package/dist/azure/financialAuthorityViewValidation.d.ts.map +1 -1
  22. package/dist/azure/financialAuthorityViewValidation.js +39 -0
  23. package/dist/azure/financialAuthorityViewValidation.js.map +1 -1
  24. package/dist/azure/financialChargeComposition.d.ts +81 -0
  25. package/dist/azure/financialChargeComposition.d.ts.map +1 -0
  26. package/dist/azure/financialChargeComposition.js +41 -0
  27. package/dist/azure/financialChargeComposition.js.map +1 -0
  28. package/dist/azure/financialChargeCompositionKernel.d.ts +3 -0
  29. package/dist/azure/financialChargeCompositionKernel.d.ts.map +1 -0
  30. package/dist/azure/financialChargeCompositionKernel.js +49 -0
  31. package/dist/azure/financialChargeCompositionKernel.js.map +1 -0
  32. package/dist/azure/financialChargeCompositionValidation.d.ts +8 -0
  33. package/dist/azure/financialChargeCompositionValidation.d.ts.map +1 -0
  34. package/dist/azure/financialChargeCompositionValidation.js +164 -0
  35. package/dist/azure/financialChargeCompositionValidation.js.map +1 -0
  36. package/dist/azure/financialDataflow.d.ts +45 -2
  37. package/dist/azure/financialDataflow.d.ts.map +1 -1
  38. package/dist/azure/financialDataflow.js.map +1 -1
  39. package/dist/azure/financialDataflowKernel.d.ts +31 -0
  40. package/dist/azure/financialDataflowKernel.d.ts.map +1 -0
  41. package/dist/azure/financialDataflowKernel.js +169 -0
  42. package/dist/azure/financialDataflowKernel.js.map +1 -0
  43. package/dist/azure/financialDataflowValidation.d.ts +5 -2
  44. package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
  45. package/dist/azure/financialDataflowValidation.js +203 -30
  46. package/dist/azure/financialDataflowValidation.js.map +1 -1
  47. package/dist/azure/financialEvidenceAssessmentValidation.d.ts.map +1 -1
  48. package/dist/azure/financialEvidenceAssessmentValidation.js +7 -0
  49. package/dist/azure/financialEvidenceAssessmentValidation.js.map +1 -1
  50. package/dist/azure/financialPolicy.d.ts +19 -3
  51. package/dist/azure/financialPolicy.d.ts.map +1 -1
  52. package/dist/azure/financialPolicy.js.map +1 -1
  53. package/dist/azure/financialPolicyDelivery.d.ts +75 -1
  54. package/dist/azure/financialPolicyDelivery.d.ts.map +1 -1
  55. package/dist/azure/financialPolicyDelivery.js +5 -1
  56. package/dist/azure/financialPolicyDelivery.js.map +1 -1
  57. package/dist/azure/financialPolicyDeliveryValidation.d.ts +16 -2
  58. package/dist/azure/financialPolicyDeliveryValidation.d.ts.map +1 -1
  59. package/dist/azure/financialPolicyDeliveryValidation.js +238 -5
  60. package/dist/azure/financialPolicyDeliveryValidation.js.map +1 -1
  61. package/dist/azure/financialPolicyKernel.d.ts +38 -0
  62. package/dist/azure/financialPolicyKernel.d.ts.map +1 -0
  63. package/dist/azure/financialPolicyKernel.js +232 -0
  64. package/dist/azure/financialPolicyKernel.js.map +1 -0
  65. package/dist/azure/financialPolicyValidation.d.ts +1 -0
  66. package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
  67. package/dist/azure/financialPolicyValidation.js +111 -16
  68. package/dist/azure/financialPolicyValidation.js.map +1 -1
  69. package/dist/azure/financialSavingsAuthority.d.ts +29 -0
  70. package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
  71. package/dist/azure/financialSavingsAuthority.js +2 -1
  72. package/dist/azure/financialSavingsAuthority.js.map +1 -1
  73. package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
  74. package/dist/azure/financialSavingsAuthorityValidation.js +3 -0
  75. package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
  76. package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
  77. package/dist/azure/financialSavingsCoordinateValidation.js +36 -0
  78. package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
  79. package/dist/azure/financialScope.d.ts +6 -0
  80. package/dist/azure/financialScope.d.ts.map +1 -1
  81. package/dist/azure/financialScope.js +6 -0
  82. package/dist/azure/financialScope.js.map +1 -1
  83. package/dist/azure/financialScopeBaseline.d.ts +8 -2
  84. package/dist/azure/financialScopeBaseline.d.ts.map +1 -1
  85. package/dist/azure/financialScopeBaseline.js +14 -1
  86. package/dist/azure/financialScopeBaseline.js.map +1 -1
  87. package/dist/azure/financialScopeBaselineValidation.d.ts.map +1 -1
  88. package/dist/azure/financialScopeBaselineValidation.js +15 -1
  89. package/dist/azure/financialScopeBaselineValidation.js.map +1 -1
  90. package/dist/azure/financialScopeEvidence.d.ts +1 -1
  91. package/dist/azure/financialScopeEvidence.d.ts.map +1 -1
  92. package/dist/azure/metrics.d.ts +2 -2
  93. package/dist/azure/metrics.d.ts.map +1 -1
  94. package/dist/azure/prices.d.ts +16 -3
  95. package/dist/azure/prices.d.ts.map +1 -1
  96. package/dist/azure/recommendations.d.ts +12 -4
  97. package/dist/azure/recommendations.d.ts.map +1 -1
  98. package/dist/azure/reports.d.ts +3 -2
  99. package/dist/azure/reports.d.ts.map +1 -1
  100. package/dist/azure/reports.js.map +1 -1
  101. package/dist/azure/resources.d.ts +12 -0
  102. package/dist/azure/resources.d.ts.map +1 -1
  103. package/dist/azure/subscriptions.d.ts +4 -2
  104. package/dist/azure/subscriptions.d.ts.map +1 -1
  105. package/dist/azure/views.d.ts +21 -5
  106. package/dist/azure/views.d.ts.map +1 -1
  107. package/dist/azure/views.js.map +1 -1
  108. package/dist/esm/azure/financialAnalyticsDelivery.js +5 -0
  109. package/dist/esm/azure/financialAnalyticsDeliveryValidation.js +184 -11
  110. package/dist/esm/azure/financialAnalyticsDisplayKernel.js +139 -0
  111. package/dist/esm/azure/financialAnalyticsValidation.js +101 -19
  112. package/dist/esm/azure/financialAuthorityViewValidation.js +39 -0
  113. package/dist/esm/azure/financialChargeComposition.js +36 -0
  114. package/dist/esm/azure/financialChargeCompositionKernel.js +44 -0
  115. package/dist/esm/azure/financialChargeCompositionValidation.js +155 -0
  116. package/dist/esm/azure/financialDataflowKernel.js +163 -0
  117. package/dist/esm/azure/financialDataflowValidation.js +199 -29
  118. package/dist/esm/azure/financialEvidenceAssessmentValidation.js +7 -0
  119. package/dist/esm/azure/financialPolicyDelivery.js +4 -0
  120. package/dist/esm/azure/financialPolicyDeliveryValidation.js +227 -7
  121. package/dist/esm/azure/financialPolicyKernel.js +225 -0
  122. package/dist/esm/azure/financialPolicyValidation.js +110 -16
  123. package/dist/esm/azure/financialSavingsAuthority.js +1 -0
  124. package/dist/esm/azure/financialSavingsAuthorityValidation.js +3 -0
  125. package/dist/esm/azure/financialSavingsCoordinateValidation.js +36 -0
  126. package/dist/esm/azure/financialScope.js +6 -0
  127. package/dist/esm/azure/financialScopeBaseline.js +12 -0
  128. package/dist/esm/azure/financialScopeBaselineValidation.js +15 -1
  129. package/dist/events/cost.d.ts +4 -0
  130. package/dist/events/cost.d.ts.map +1 -1
  131. package/package.json +3 -2
@@ -0,0 +1,139 @@
1
+ import { formatExactDecimalValue, parseCanonicalDecimal, sumCanonicalDecimals } from '../common/exactDecimal.js';
2
+ import { isFinancialAnalyticsInputSeriesV1 } from './financialAnalyticsValidation.js';
3
+ const DAY_MS = 86400000;
4
+ const HISTORY_NOT_PRODUCED = 'history-not-produced';
5
+ const CALENDAR_PERIOD_INCOMPLETE = 'calendar-period-incomplete';
6
+ const addUtcDays = (date, days) => new Date(Date.parse(`${date}T00:00:00.000Z`) + days * DAY_MS).toISOString().slice(0, 10);
7
+ const shiftUtcMonths = (date, months) => {
8
+ const parsed = new Date(`${date}T00:00:00.000Z`);
9
+ return new Date(Date.UTC(parsed.getUTCFullYear(), parsed.getUTCMonth() + months, 1)).toISOString().slice(0, 10);
10
+ };
11
+ const uniqueReasons = (values) => {
12
+ const reasons = [...new Set(values)].sort();
13
+ return (reasons.length > 0 ? reasons : [HISTORY_NOT_PRODUCED]);
14
+ };
15
+ /** Twelve-decimal half-away-from-zero display average; totals remain exact. */
16
+ const averageKnownPerDay = (amount, dayCount) => {
17
+ if (!Number.isSafeInteger(dayCount) || dayCount < 1)
18
+ throw new RangeError('Display average requires a positive day count.');
19
+ const value = parseCanonicalDecimal(amount);
20
+ const precision = 12;
21
+ const numerator = (value.coefficient < 0n ? -value.coefficient : value.coefficient) * 10n ** BigInt(precision);
22
+ const scaledDivisor = BigInt(dayCount) * 10n ** BigInt(value.scale);
23
+ let quotient = numerator / scaledDivisor;
24
+ const remainder = numerator % scaledDivisor;
25
+ if (remainder * 2n >= scaledDivisor)
26
+ quotient += 1n;
27
+ return formatExactDecimalValue({ coefficient: value.coefficient < 0n ? -quotient : quotient, scale: precision });
28
+ };
29
+ const requestedInterval = (input, periodKey) => {
30
+ const endDateExclusive = input.coordinate.period.requested.endDateExclusive;
31
+ const startDate = periodKey === 'last-7-days'
32
+ ? addUtcDays(endDateExclusive, -7)
33
+ : periodKey === 'rolling-30-days'
34
+ ? addUtcDays(endDateExclusive, -30)
35
+ : periodKey === 'rolling-90-days'
36
+ ? addUtcDays(endDateExclusive, -90)
37
+ : shiftUtcMonths(addUtcDays(endDateExclusive, -1), -11);
38
+ return {
39
+ startDate,
40
+ endDateExclusive,
41
+ dateBasis: input.coordinate.period.requested.dateBasis,
42
+ ...(input.coordinate.period.requested.timeZone === undefined
43
+ ? {}
44
+ : { timeZone: input.coordinate.period.requested.timeZone }),
45
+ };
46
+ };
47
+ const displayDays = (input, startDate, endDateExclusive) => {
48
+ const pointByDate = new Map(input.points.map(point => [point.date, point]));
49
+ const gapReasons = (date) => input.gaps
50
+ .filter(gap => gap.startDate <= date && gap.endDateExclusive > date)
51
+ .flatMap(gap => gap.reasonCodes);
52
+ const days = [];
53
+ for (let date = startDate; date < endDateExclusive; date = addUtcDays(date, 1)) {
54
+ const point = pointByDate.get(date);
55
+ if (point?.status === 'available') {
56
+ days.push({ date, status: 'available', knownAmount: point.amount, reasonCodes: [] });
57
+ }
58
+ else if (point?.status === 'partial') {
59
+ days.push({ date, status: 'partial', knownAmount: point.knownAmount, reasonCodes: point.reasonCodes });
60
+ }
61
+ else {
62
+ const reasons = gapReasons(date);
63
+ days.push({ date, status: 'unavailable', reasonCodes: reasons.length > 0 ? reasons : [HISTORY_NOT_PRODUCED] });
64
+ }
65
+ }
66
+ return days;
67
+ };
68
+ const toBucket = (bucketKey, startDate, endDateExclusive, days) => {
69
+ const knownAmounts = days.flatMap(day => (day.knownAmount === undefined ? [] : [day.knownAmount]));
70
+ const coversWholeBucket = days[0]?.date === startDate && days.length > 0 && addUtcDays(days[days.length - 1].date, 1) === endDateExclusive;
71
+ const reasonCodes = uniqueReasons([
72
+ ...days.flatMap(day => day.reasonCodes),
73
+ ...(coversWholeBucket ? [] : [CALENDAR_PERIOD_INCOMPLETE]),
74
+ ]);
75
+ if (knownAmounts.length === 0) {
76
+ return { bucketKey, startDate, endDateExclusive, status: 'unavailable', reasonCodes };
77
+ }
78
+ const knownAmount = formatExactDecimalValue(sumCanonicalDecimals(knownAmounts));
79
+ const average = averageKnownPerDay(knownAmount, knownAmounts.length);
80
+ if (coversWholeBucket && days.every(day => day.status === 'available')) {
81
+ return { bucketKey, startDate, endDateExclusive, status: 'available', amount: knownAmount, averageKnownPerDay: average };
82
+ }
83
+ return { bucketKey, startDate, endDateExclusive, status: 'partial', knownAmount, averageKnownPerDay: average, reasonCodes };
84
+ };
85
+ const dailyBuckets = (days) => days.map(day => toBucket(day.date, day.date, addUtcDays(day.date, 1), [day]));
86
+ const monthlyBuckets = (days) => {
87
+ const byMonth = new Map();
88
+ days.forEach(day => {
89
+ const month = day.date.slice(0, 7);
90
+ byMonth.set(month, [...(byMonth.get(month) ?? []), day]);
91
+ });
92
+ return [...byMonth.entries()].map(([month, monthDays]) => toBucket(month, `${month}-01`, shiftUtcMonths(`${month}-01`, 1), monthDays));
93
+ };
94
+ /**
95
+ * Exact, non-authoritative display rollup over one producer-owned analytics
96
+ * input. The function never changes charge inclusion, estimates, cost basis,
97
+ * currency, or source evidence; uncovered dates remain explicit.
98
+ */
99
+ export const projectFinancialAnalyticsDisplayPeriodV1 = (request) => {
100
+ if (!isFinancialAnalyticsInputSeriesV1(request.input)) {
101
+ throw new TypeError('Financial analytics display projection requires a valid input series.');
102
+ }
103
+ const current = request.input.referenceCompositions.find(composition => composition.coordinate.periodRole === 'current-spend' && composition.compositionId === request.currentSpendCompositionId);
104
+ if (!current) {
105
+ throw new TypeError('Financial analytics input does not bind the requested current-spend composition.');
106
+ }
107
+ const accountingCurrency = request.input.coordinate.accountingCurrency;
108
+ if (accountingCurrency.status !== 'resolved') {
109
+ throw new TypeError('Financial analytics display projection requires a resolved accounting currency.');
110
+ }
111
+ const requested = requestedInterval(request.input, request.periodKey);
112
+ const days = displayDays(request.input, requested.startDate, requested.endDateExclusive);
113
+ const buckets = request.periodKey === 'trailing-12-calendar-months' ? monthlyBuckets(days) : dailyBuckets(days);
114
+ const common = {
115
+ analyticsInputId: request.input.analyticsInputId,
116
+ currentSpendCompositionId: current.compositionId,
117
+ chargeInclusionPolicyRef: current.coordinate.chargeInclusionPolicyRef,
118
+ periodKey: request.periodKey,
119
+ requested,
120
+ bucketGranularity: request.periodKey === 'trailing-12-calendar-months' ? 'calendar-month' : 'daily',
121
+ currencyCode: accountingCurrency.currencyCode,
122
+ buckets,
123
+ };
124
+ const knownAmounts = days.flatMap(day => (day.knownAmount === undefined ? [] : [day.knownAmount]));
125
+ const calendarPeriodIncomplete = request.periodKey === 'trailing-12-calendar-months' &&
126
+ requested.endDateExclusive !== shiftUtcMonths(addUtcDays(requested.endDateExclusive, -1), 1);
127
+ const reasonCodes = uniqueReasons([
128
+ ...days.flatMap(day => day.reasonCodes),
129
+ ...(calendarPeriodIncomplete ? [CALENDAR_PERIOD_INCOMPLETE] : []),
130
+ ]);
131
+ if (knownAmounts.length === 0)
132
+ return { ...common, status: 'unavailable', reasonCodes };
133
+ const knownAmount = formatExactDecimalValue(sumCanonicalDecimals(knownAmounts));
134
+ const average = averageKnownPerDay(knownAmount, knownAmounts.length);
135
+ if (!calendarPeriodIncomplete && days.every(day => day.status === 'available')) {
136
+ return { ...common, status: 'available', amount: knownAmount, averageKnownPerDay: average };
137
+ }
138
+ return { ...common, status: 'partial', knownAmount, averageKnownPerDay: average, reasonCodes };
139
+ };
@@ -6,6 +6,7 @@ import { FINANCIAL_DATAFLOW_LIMITS_V1, canonicalizeFinancialDataflowJsonV1, hasF
6
6
  import { isCurrentSpendCompositionV1 } from './financialDataflowValidation.js';
7
7
  const MAX_POINTS = 3660;
8
8
  const MAX_GAPS = 3660;
9
+ const MAX_REFERENCE_COMPOSITIONS = 2;
9
10
  const isReasonCodes = (value) => Array.isArray(value) &&
10
11
  value.length > 0 &&
11
12
  value.length <= FINANCIAL_DATAFLOW_LIMITS_V1.maximumReasonCodes &&
@@ -29,6 +30,7 @@ const comparableCoordinate = (value) => ({
29
30
  estimateLens: value.estimateLens,
30
31
  requestedCurrencyCode: value.requestedCurrencyCode,
31
32
  accountingCurrency: value.accountingCurrency,
33
+ chargeInclusionPolicyRef: value.chargeInclusionPolicyRef,
32
34
  });
33
35
  const hasSameFinancialDimensions = (left, right) => canonicalizeFinancialDataflowJsonV1(comparableCoordinate(left)) === canonicalizeFinancialDataflowJsonV1(comparableCoordinate(right));
34
36
  const hasComparableTrendPeriods = (current, comparison) => {
@@ -42,6 +44,10 @@ const hasComparableTrendPeriods = (current, comparison) => {
42
44
  return (intervalDayCount(current.period.requested.startDate, current.period.requested.endDateExclusive) ===
43
45
  intervalDayCount(comparison.period.requested.startDate, comparison.period.requested.endDateExclusive));
44
46
  };
47
+ const periodContains = (container, target) => container.period.requested.dateBasis === target.period.requested.dateBasis &&
48
+ container.period.requested.timeZone === target.period.requested.timeZone &&
49
+ container.period.requested.startDate <= target.period.requested.startDate &&
50
+ container.period.requested.endDateExclusive >= target.period.requested.endDateExclusive;
45
51
  const isInputIdentity = (value) => {
46
52
  if (!isFinancialDataflowRecordV1(value) ||
47
53
  !hasFinancialDataflowExactFieldsV1(value, [
@@ -50,6 +56,7 @@ const isInputIdentity = (value) => {
50
56
  'coordinate',
51
57
  'granularity',
52
58
  'producerGenerationId',
59
+ 'referenceCompositions',
53
60
  'points',
54
61
  'gaps',
55
62
  'coverage',
@@ -59,9 +66,12 @@ const isInputIdentity = (value) => {
59
66
  value.contractVersion !== FINANCIAL_ANALYTICS_INPUT_CONTRACT_VERSION_V1 ||
60
67
  !isFinancialDataflowCoordinateV1(value.coordinate) ||
61
68
  value.coordinate.periodRole !== 'analytics-input' ||
69
+ value.coordinate.period.windowKind !== 'analytics-history' ||
62
70
  value.coordinate.accountingCurrency.status !== 'resolved' ||
63
71
  value.granularity !== 'daily' ||
64
72
  !isFinancialDataflowIdentityV1(value.producerGenerationId) ||
73
+ !Array.isArray(value.referenceCompositions) ||
74
+ value.referenceCompositions.length > MAX_REFERENCE_COMPOSITIONS ||
65
75
  !Array.isArray(value.points) ||
66
76
  value.points.length > MAX_POINTS ||
67
77
  !Array.isArray(value.gaps) ||
@@ -71,6 +81,16 @@ const isInputIdentity = (value) => {
71
81
  !isFinancialDataflowIdentityV1(value.algorithmVersion)) {
72
82
  return false;
73
83
  }
84
+ const coordinate = value.coordinate;
85
+ const referenceCompositions = value.referenceCompositions;
86
+ if (!referenceCompositions.every(isCurrentSpendCompositionV1))
87
+ return false;
88
+ const referenceRoles = referenceCompositions.map(composition => composition.coordinate.periodRole);
89
+ if (new Set(referenceCompositions.map(composition => composition.compositionId)).size !== referenceCompositions.length ||
90
+ new Set(referenceRoles).size !== referenceRoles.length ||
91
+ referenceCompositions.some(composition => !hasSameFinancialDimensions(coordinate, composition.coordinate))) {
92
+ return false;
93
+ }
74
94
  const gaps = value.gaps;
75
95
  const requested = value.coordinate.period.requested;
76
96
  const dates = new Set();
@@ -86,16 +106,47 @@ const isInputIdentity = (value) => {
86
106
  return false;
87
107
  }
88
108
  dates.add(point.date);
109
+ const pointForecastFields = [
110
+ point.forecastEligibleAmount,
111
+ point.oneTimeAmount,
112
+ point.unknownRecurrenceAmount,
113
+ ];
114
+ if (!pointForecastFields.every(amount => isCanonicalExactMoney({ amount, currencyCode: coordinate.accountingCurrency.currencyCode })) ||
115
+ (point.forecastStatus !== 'available' && point.forecastStatus !== 'partial') ||
116
+ (point.forecastStatus === 'available'
117
+ ? point.forecastReasonCodes !== undefined || point.unknownRecurrenceAmount !== '0'
118
+ : !isReasonCodes(point.forecastReasonCodes))) {
119
+ return false;
120
+ }
89
121
  if (point.status === 'available') {
90
- if (!hasFinancialDataflowExactFieldsV1(point, ['date', 'compositionId', 'status', 'amount']) ||
91
- !isCanonicalExactMoney({ amount: point.amount, currencyCode: 'AUD' }))
122
+ if (!hasFinancialDataflowExactFieldsV1(point, [
123
+ 'date',
124
+ 'compositionId',
125
+ 'status',
126
+ 'amount',
127
+ 'forecastEligibleAmount',
128
+ 'oneTimeAmount',
129
+ 'unknownRecurrenceAmount',
130
+ 'forecastStatus',
131
+ ], ['forecastReasonCodes']) ||
132
+ !isCanonicalExactMoney({ amount: point.amount, currencyCode: coordinate.accountingCurrency.currencyCode }))
92
133
  return false;
93
134
  availableDayCount += 1;
94
135
  }
95
136
  else {
96
137
  if (point.status !== 'partial' ||
97
- !hasFinancialDataflowExactFieldsV1(point, ['date', 'compositionId', 'status', 'knownAmount', 'reasonCodes']) ||
98
- !isCanonicalExactMoney({ amount: point.knownAmount, currencyCode: 'AUD' }) ||
138
+ !hasFinancialDataflowExactFieldsV1(point, [
139
+ 'date',
140
+ 'compositionId',
141
+ 'status',
142
+ 'knownAmount',
143
+ 'reasonCodes',
144
+ 'forecastEligibleAmount',
145
+ 'oneTimeAmount',
146
+ 'unknownRecurrenceAmount',
147
+ 'forecastStatus',
148
+ ], ['forecastReasonCodes']) ||
149
+ !isCanonicalExactMoney({ amount: point.knownAmount, currencyCode: coordinate.accountingCurrency.currencyCode }) ||
99
150
  !isReasonCodes(point.reasonCodes))
100
151
  return false;
101
152
  partialDayCount += 1;
@@ -143,9 +194,7 @@ const isInputIdentity = (value) => {
143
194
  value.coverage.partialDayCount === partialDayCount &&
144
195
  value.coverage.unavailableDayCount === unavailableDayCount &&
145
196
  availableDayCount + partialDayCount + unavailableDayCount === expectedDays &&
146
- cursor === requested.endDateExclusive &&
147
- value.coordinate.period.gaps.length === gaps.length &&
148
- value.coordinate.period.gaps.every(periodGap => gaps.some(gap => gap.startDate === periodGap.startDate && gap.endDateExclusive === periodGap.endDateExclusive)));
197
+ cursor === requested.endDateExclusive);
149
198
  };
150
199
  export const canonicalizeFinancialAnalyticsInputIdentityV1 = (value) => {
151
200
  if (!isFinancialDataflowValueWithinLimitsV1(value) || !isInputIdentity(value)) {
@@ -154,9 +203,16 @@ export const canonicalizeFinancialAnalyticsInputIdentityV1 = (value) => {
154
203
  return canonicalizeFinancialDataflowJsonV1({
155
204
  ...value,
156
205
  coordinate: { ...value.coordinate, providerAccountRefs: [...value.coordinate.providerAccountRefs].sort() },
206
+ referenceCompositions: [...value.referenceCompositions].sort((left, right) => left.compositionId.localeCompare(right.compositionId)),
157
207
  points: [...value.points]
158
208
  .sort((left, right) => left.date.localeCompare(right.date))
159
- .map(point => (point.status === 'partial' ? { ...point, reasonCodes: [...point.reasonCodes].sort() } : point)),
209
+ .map(point => ({
210
+ ...point,
211
+ ...(point.status === 'partial' ? { reasonCodes: [...point.reasonCodes].sort() } : {}),
212
+ ...(point.forecastReasonCodes === undefined
213
+ ? {}
214
+ : { forecastReasonCodes: [...point.forecastReasonCodes].sort() }),
215
+ })),
160
216
  gaps: [...value.gaps]
161
217
  .sort((left, right) => `${left.startDate}\u0000${left.endDateExclusive}`.localeCompare(`${right.startDate}\u0000${right.endDateExclusive}`))
162
218
  .map(gap => ({ ...gap, reasonCodes: [...gap.reasonCodes].sort() })),
@@ -173,6 +229,18 @@ export const isFinancialAnalyticsInputSeriesV1 = (value) => {
173
229
  };
174
230
  const nextCalendarDate = (value) => new Date(Date.parse(`${value}T00:00:00.000Z`) + 86400000).toISOString().slice(0, 10);
175
231
  const hasSameReasonCodes = (left, right) => left.length === right.length && left.every(reasonCode => right.includes(reasonCode));
232
+ const hasSameForecastSlice = (point, composition) => {
233
+ const selection = composition.chargeSelection;
234
+ return selection !== undefined &&
235
+ point.forecastEligibleAmount === selection.forecastEligibleAmount &&
236
+ point.oneTimeAmount === selection.oneTimeAmount &&
237
+ point.unknownRecurrenceAmount === selection.unknownRecurrenceAmount &&
238
+ point.forecastStatus === selection.forecastStatus &&
239
+ (point.forecastReasonCodes === undefined
240
+ ? selection.forecastReasonCodes === undefined
241
+ : selection.forecastReasonCodes !== undefined &&
242
+ hasSameReasonCodes(point.forecastReasonCodes, selection.forecastReasonCodes));
243
+ };
176
244
  /** Proves that every analytics point is an exact projection of its referenced daily composition. */
177
245
  export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyCompositions) => {
178
246
  if (!isFinancialAnalyticsInputSeriesV1(input) ||
@@ -188,6 +256,7 @@ export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyComposit
188
256
  return input.points.every(point => {
189
257
  const composition = compositionById.get(point.compositionId);
190
258
  if (composition === undefined ||
259
+ composition.amount.status === 'unavailable' ||
191
260
  composition.coordinate.periodRole !== 'current-spend' ||
192
261
  composition.coordinate.period.windowKind !== 'daily' ||
193
262
  composition.coordinate.period.requested.startDate !== point.date ||
@@ -196,11 +265,14 @@ export const isFinancialAnalyticsInputSeriesCompatibleV1 = (input, dailyComposit
196
265
  return false;
197
266
  }
198
267
  if (point.status === 'available') {
199
- return composition.amount.status === 'available' && point.amount === composition.amount.amount;
268
+ return (composition.amount.status === 'available' &&
269
+ point.amount === composition.amount.amount &&
270
+ hasSameForecastSlice(point, composition));
200
271
  }
201
272
  return (composition.amount.status === 'partial' &&
202
273
  point.knownAmount === composition.amount.knownAmount &&
203
- hasSameReasonCodes(point.reasonCodes, composition.amount.reasonCodes));
274
+ hasSameReasonCodes(point.reasonCodes, composition.amount.reasonCodes) &&
275
+ hasSameForecastSlice(point, composition));
204
276
  });
205
277
  };
206
278
  const isResult = (value, currency) => {
@@ -234,7 +306,6 @@ const isResult = (value, currency) => {
234
306
  return (value.kind === 'anomaly' &&
235
307
  hasFinancialDataflowExactFieldsV1(value, ['kind', 'events']) &&
236
308
  Array.isArray(value.events) &&
237
- value.events.length > 0 &&
238
309
  value.events.length <= MAX_POINTS &&
239
310
  value.events.every(event => {
240
311
  if (!isFinancialDataflowRecordV1(event) ||
@@ -283,9 +354,9 @@ const isProjectionIdentity = (value) => {
283
354
  return false;
284
355
  const currency = coordinateCurrency(value.coordinate);
285
356
  return (isFinancialDataflowHashV1(value.analyticsInputId) &&
357
+ isFinancialDataflowHashV1(value.currentSpendCompositionId) &&
286
358
  currency !== undefined &&
287
- isResult(value.result, currency) &&
288
- (value.result.kind === 'anomaly' || isFinancialDataflowHashV1(value.currentSpendCompositionId)));
359
+ isResult(value.result, currency));
289
360
  };
290
361
  export const canonicalizeFinancialAnalyticsProjectionIdentityV1 = (value) => {
291
362
  if (!isFinancialDataflowValueWithinLimitsV1(value) || !isProjectionIdentity(value)) {
@@ -334,17 +405,24 @@ export const isFinancialAnalyticsProjectionCompatibleV1 = (projection, input, cu
334
405
  hasSameFinancialDimensions(projection.coordinate, currentSpendComposition.coordinate)));
335
406
  }
336
407
  if (projection.result.kind === 'anomaly') {
337
- if (projection.currentSpendCompositionId !== undefined &&
338
- (!isCurrentSpendCompositionV1(currentSpendComposition) ||
339
- projection.currentSpendCompositionId !== currentSpendComposition.compositionId ||
340
- !hasSameFinancialDimensions(projection.coordinate, currentSpendComposition.coordinate))) {
408
+ if (!isCurrentSpendCompositionV1(currentSpendComposition) ||
409
+ projection.currentSpendCompositionId !== currentSpendComposition.compositionId ||
410
+ !hasSameFinancialDimensions(projection.coordinate, currentSpendComposition.coordinate) ||
411
+ canonicalizeFinancialDataflowCoordinateV1({ ...projection.coordinate, periodRole: 'current-spend' }) !==
412
+ canonicalizeFinancialDataflowCoordinateV1(currentSpendComposition.coordinate) ||
413
+ !periodContains(input.coordinate, projection.coordinate)) {
341
414
  return false;
342
415
  }
343
416
  const pointsByDate = new Map(input.points.map(point => [point.date, point]));
344
417
  const eventDates = new Set();
418
+ const targetPeriod = projection.coordinate.period.requested;
345
419
  return projection.result.events.every(event => {
346
420
  const point = pointsByDate.get(event.date);
347
- if (point === undefined || eventDates.has(event.date) || (projection.status === 'available' && point.status !== 'available'))
421
+ if (event.date < targetPeriod.startDate ||
422
+ event.date >= targetPeriod.endDateExclusive ||
423
+ point === undefined ||
424
+ eventDates.has(event.date) ||
425
+ (projection.status === 'available' && point.status !== 'available'))
348
426
  return false;
349
427
  eventDates.add(event.date);
350
428
  const observedAmount = point.status === 'available' ? point.amount : point.knownAmount;
@@ -394,7 +472,11 @@ export const isFinancialAnalyticsProjectionCompatibleV1 = (projection, input, cu
394
472
  canonicalizeFinancialDataflowCoordinateV1(currentSpendComposition.coordinate)) {
395
473
  return false;
396
474
  }
397
- if (projection.status === 'available' && currentSpendComposition.amount.status !== 'available')
475
+ if (projection.status === 'available' &&
476
+ currentSpendComposition.amount.status !== 'available' &&
477
+ !(currentSpendComposition.amount.status === 'partial' &&
478
+ currentSpendComposition.amount.reasonCodes.length === 1 &&
479
+ currentSpendComposition.amount.reasonCodes[0] === 'coverage-incomplete'))
398
480
  return false;
399
481
  if (currentSpendComposition.amount.status === 'unavailable')
400
482
  return false;
@@ -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
+ };