@spottoai/types-package 1.0.2-dev1138.2 → 1.0.2-dev1138.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +9 -0
  2. package/dist/accounts/accounts.d.ts +77 -4
  3. package/dist/accounts/accounts.d.ts.map +1 -1
  4. package/dist/accounts/accounts.js +2 -1
  5. package/dist/accounts/accounts.js.map +1 -1
  6. package/dist/accounts/azureManualOnboarding.d.ts +114 -0
  7. package/dist/accounts/azureManualOnboarding.d.ts.map +1 -0
  8. package/dist/accounts/azureManualOnboarding.js +12 -0
  9. package/dist/accounts/azureManualOnboarding.js.map +1 -0
  10. package/dist/accounts/azureSpSetup.d.ts +128 -31
  11. package/dist/accounts/azureSpSetup.d.ts.map +1 -1
  12. package/dist/accounts/azureSpSetup.js +15 -0
  13. package/dist/accounts/azureSpSetup.js.map +1 -1
  14. package/dist/accounts/azureSpSetupDurable.d.ts +8 -0
  15. package/dist/accounts/azureSpSetupDurable.d.ts.map +1 -1
  16. package/dist/accounts/azureSpSetupDurable.js +28 -0
  17. package/dist/accounts/azureSpSetupDurable.js.map +1 -1
  18. package/dist/accounts/index.d.ts +1 -0
  19. package/dist/accounts/index.d.ts.map +1 -1
  20. package/dist/accounts/index.js +1 -0
  21. package/dist/accounts/index.js.map +1 -1
  22. package/dist/accounts/readPermissions.d.ts +1 -1
  23. package/dist/accounts/readPermissions.d.ts.map +1 -1
  24. package/dist/accounts/readPermissions.js.map +1 -1
  25. package/dist/accounts/validation.d.ts +24 -0
  26. package/dist/accounts/validation.d.ts.map +1 -1
  27. package/dist/ai/index.d.ts +14 -1
  28. package/dist/ai/index.d.ts.map +1 -1
  29. package/dist/ai/index.js +12 -0
  30. package/dist/ai/index.js.map +1 -1
  31. package/dist/aws/commitments-planning.d.ts +5 -0
  32. package/dist/aws/commitments-planning.d.ts.map +1 -1
  33. package/dist/aws/commitments-planning.js +19 -0
  34. package/dist/aws/commitments-planning.js.map +1 -1
  35. package/dist/aws/estates.d.ts +1 -1
  36. package/dist/aws/estates.d.ts.map +1 -1
  37. package/dist/aws/estates.js +7 -1
  38. package/dist/aws/estates.js.map +1 -1
  39. package/dist/aws/index.d.ts +5 -0
  40. package/dist/aws/index.d.ts.map +1 -1
  41. package/dist/aws/index.js +5 -0
  42. package/dist/aws/index.js.map +1 -1
  43. package/dist/aws/onboarding.d.ts +3 -0
  44. package/dist/aws/onboarding.d.ts.map +1 -1
  45. package/dist/aws/organizationCommitments.d.ts +260 -0
  46. package/dist/aws/organizationCommitments.d.ts.map +1 -0
  47. package/dist/aws/organizationCommitments.js +53 -0
  48. package/dist/aws/organizationCommitments.js.map +1 -0
  49. package/dist/aws/organizationCommitmentsApiValidation.d.ts +21 -0
  50. package/dist/aws/organizationCommitmentsApiValidation.d.ts.map +1 -0
  51. package/dist/aws/organizationCommitmentsApiValidation.js +226 -0
  52. package/dist/aws/organizationCommitmentsApiValidation.js.map +1 -0
  53. package/dist/aws/organizationCommitmentsValidation.d.ts +12 -0
  54. package/dist/aws/organizationCommitmentsValidation.d.ts.map +1 -0
  55. package/dist/aws/organizationCommitmentsValidation.js +228 -0
  56. package/dist/aws/organizationCommitmentsValidation.js.map +1 -0
  57. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.d.ts +9 -0
  58. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.d.ts.map +1 -0
  59. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js +407 -0
  60. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js.map +1 -0
  61. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.d.ts +17 -0
  62. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.d.ts.map +1 -0
  63. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js +6 -0
  64. package/dist/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js.map +1 -0
  65. package/dist/aws/requests.d.ts +13 -2
  66. package/dist/aws/requests.d.ts.map +1 -1
  67. package/dist/aws/requests.js +1 -1
  68. package/dist/aws/requests.js.map +1 -1
  69. package/dist/azure/commitmentsPlanning.d.ts +2 -2
  70. package/dist/azure/commitmentsPlanning.d.ts.map +1 -1
  71. package/dist/azure/financialAnalytics.d.ts +68 -2
  72. package/dist/azure/financialAnalytics.d.ts.map +1 -1
  73. package/dist/azure/financialAnalyticsDelivery.d.ts +2 -0
  74. package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
  75. package/dist/azure/financialAnalyticsDelivery.js +4 -1
  76. package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
  77. package/dist/azure/financialAnalyticsDisplayKernel.d.ts +12 -0
  78. package/dist/azure/financialAnalyticsDisplayKernel.d.ts.map +1 -0
  79. package/dist/azure/financialAnalyticsDisplayKernel.js +144 -0
  80. package/dist/azure/financialAnalyticsDisplayKernel.js.map +1 -0
  81. package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
  82. package/dist/azure/financialAnalyticsValidation.js +61 -7
  83. package/dist/azure/financialAnalyticsValidation.js.map +1 -1
  84. package/dist/azure/financialAuthorityProjectionKernel.d.ts +7 -0
  85. package/dist/azure/financialAuthorityProjectionKernel.d.ts.map +1 -0
  86. package/dist/azure/financialAuthorityProjectionKernel.js +165 -0
  87. package/dist/azure/financialAuthorityProjectionKernel.js.map +1 -0
  88. package/dist/azure/financialAuthorityView.d.ts +4 -0
  89. package/dist/azure/financialAuthorityView.d.ts.map +1 -1
  90. package/dist/azure/financialAuthorityView.js.map +1 -1
  91. package/dist/azure/financialAuthorityViewValidation.d.ts +3 -1
  92. package/dist/azure/financialAuthorityViewValidation.d.ts.map +1 -1
  93. package/dist/azure/financialAuthorityViewValidation.js +206 -2
  94. package/dist/azure/financialAuthorityViewValidation.js.map +1 -1
  95. package/dist/azure/financialChargeComposition.d.ts +81 -0
  96. package/dist/azure/financialChargeComposition.d.ts.map +1 -0
  97. package/dist/azure/financialChargeComposition.js +41 -0
  98. package/dist/azure/financialChargeComposition.js.map +1 -0
  99. package/dist/azure/financialChargeCompositionKernel.d.ts +3 -0
  100. package/dist/azure/financialChargeCompositionKernel.d.ts.map +1 -0
  101. package/dist/azure/financialChargeCompositionKernel.js +53 -0
  102. package/dist/azure/financialChargeCompositionKernel.js.map +1 -0
  103. package/dist/azure/financialChargeCompositionValidation.d.ts +8 -0
  104. package/dist/azure/financialChargeCompositionValidation.d.ts.map +1 -0
  105. package/dist/azure/financialChargeCompositionValidation.js +164 -0
  106. package/dist/azure/financialChargeCompositionValidation.js.map +1 -0
  107. package/dist/azure/financialDataflow.d.ts +8 -1
  108. package/dist/azure/financialDataflow.d.ts.map +1 -1
  109. package/dist/azure/financialDataflow.js.map +1 -1
  110. package/dist/azure/financialDataflowKernel.d.ts +2 -0
  111. package/dist/azure/financialDataflowKernel.d.ts.map +1 -1
  112. package/dist/azure/financialDataflowKernel.js +60 -7
  113. package/dist/azure/financialDataflowKernel.js.map +1 -1
  114. package/dist/azure/financialDataflowValidation.d.ts +1 -1
  115. package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
  116. package/dist/azure/financialDataflowValidation.js +146 -28
  117. package/dist/azure/financialDataflowValidation.js.map +1 -1
  118. package/dist/azure/financialPolicy.d.ts +3 -0
  119. package/dist/azure/financialPolicy.d.ts.map +1 -1
  120. package/dist/azure/financialPolicy.js.map +1 -1
  121. package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
  122. package/dist/azure/financialPolicyValidation.js +5 -0
  123. package/dist/azure/financialPolicyValidation.js.map +1 -1
  124. package/dist/azure/financialProjection.d.ts +11 -1
  125. package/dist/azure/financialProjection.d.ts.map +1 -1
  126. package/dist/azure/financialProjection.js.map +1 -1
  127. package/dist/azure/financialProjectionValidation.d.ts.map +1 -1
  128. package/dist/azure/financialProjectionValidation.js +43 -3
  129. package/dist/azure/financialProjectionValidation.js.map +1 -1
  130. package/dist/azure/financialSavingsAuthority.d.ts +25 -4
  131. package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
  132. package/dist/azure/financialSavingsAuthorityValidation.d.ts +6 -2
  133. package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
  134. package/dist/azure/financialSavingsAuthorityValidation.js +126 -2
  135. package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
  136. package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
  137. package/dist/azure/financialSavingsCoordinateValidation.js +62 -19
  138. package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
  139. package/dist/azure/financialSavingsSurfaceProjection.d.ts +58 -3
  140. package/dist/azure/financialSavingsSurfaceProjection.d.ts.map +1 -1
  141. package/dist/azure/financialSavingsSurfaceProjection.js +3 -1
  142. package/dist/azure/financialSavingsSurfaceProjection.js.map +1 -1
  143. package/dist/azure/financialSavingsSurfaceProjectionKernel.d.ts +14 -0
  144. package/dist/azure/financialSavingsSurfaceProjectionKernel.d.ts.map +1 -0
  145. package/dist/azure/financialSavingsSurfaceProjectionKernel.js +326 -0
  146. package/dist/azure/financialSavingsSurfaceProjectionKernel.js.map +1 -0
  147. package/dist/azure/financialSavingsSurfaceProjectionValidation.d.ts.map +1 -1
  148. package/dist/azure/financialSavingsSurfaceProjectionValidation.js +126 -12
  149. package/dist/azure/financialSavingsSurfaceProjectionValidation.js.map +1 -1
  150. package/dist/azure/financialScope.d.ts +6 -0
  151. package/dist/azure/financialScope.d.ts.map +1 -1
  152. package/dist/azure/financialScope.js +6 -0
  153. package/dist/azure/financialScope.js.map +1 -1
  154. package/dist/azure/governance.d.ts +145 -0
  155. package/dist/azure/governance.d.ts.map +1 -1
  156. package/dist/azure/governance.js +2 -1
  157. package/dist/azure/governance.js.map +1 -1
  158. package/dist/azure/metrics.d.ts +2 -2
  159. package/dist/azure/metrics.d.ts.map +1 -1
  160. package/dist/azure/payloads.d.ts +19 -0
  161. package/dist/azure/payloads.d.ts.map +1 -1
  162. package/dist/azure/prices.d.ts +16 -3
  163. package/dist/azure/prices.d.ts.map +1 -1
  164. package/dist/azure/recommendationState.d.ts +1 -1
  165. package/dist/azure/recommendationState.d.ts.map +1 -1
  166. package/dist/azure/recommendations.d.ts +15 -5
  167. package/dist/azure/recommendations.d.ts.map +1 -1
  168. package/dist/azure/reports.d.ts +3 -2
  169. package/dist/azure/reports.d.ts.map +1 -1
  170. package/dist/azure/reports.js.map +1 -1
  171. package/dist/azure/resources.d.ts +12 -0
  172. package/dist/azure/resources.d.ts.map +1 -1
  173. package/dist/azure/serviceRetirement.d.ts +33 -2
  174. package/dist/azure/serviceRetirement.d.ts.map +1 -1
  175. package/dist/azure/subscriptions.d.ts +13 -2
  176. package/dist/azure/subscriptions.d.ts.map +1 -1
  177. package/dist/azure/views.d.ts +26 -3
  178. package/dist/azure/views.d.ts.map +1 -1
  179. package/dist/azure/views.js.map +1 -1
  180. package/dist/azure/vmPatchManagement.d.ts +109 -0
  181. package/dist/azure/vmPatchManagement.d.ts.map +1 -0
  182. package/dist/azure/vmPatchManagement.js +7 -0
  183. package/dist/azure/vmPatchManagement.js.map +1 -0
  184. package/dist/common/provider.d.ts +53 -0
  185. package/dist/common/provider.d.ts.map +1 -1
  186. package/dist/common/provider.js.map +1 -1
  187. package/dist/environment/canonicalization.d.ts +12 -0
  188. package/dist/environment/canonicalization.d.ts.map +1 -0
  189. package/dist/environment/canonicalization.js +40 -0
  190. package/dist/environment/canonicalization.js.map +1 -0
  191. package/dist/environment/contracts.d.ts +196 -0
  192. package/dist/environment/contracts.d.ts.map +1 -0
  193. package/dist/environment/contracts.js +21 -0
  194. package/dist/environment/contracts.js.map +1 -0
  195. package/dist/environment/index.d.ts +5 -0
  196. package/dist/environment/index.d.ts.map +1 -0
  197. package/dist/environment/index.js +21 -0
  198. package/dist/environment/index.js.map +1 -0
  199. package/dist/environment/internal.d.ts +21 -0
  200. package/dist/environment/internal.d.ts.map +1 -0
  201. package/dist/environment/internal.js +113 -0
  202. package/dist/environment/internal.js.map +1 -0
  203. package/dist/environment/references.d.ts +18 -0
  204. package/dist/environment/references.d.ts.map +1 -0
  205. package/dist/environment/references.js +206 -0
  206. package/dist/environment/references.js.map +1 -0
  207. package/dist/environment/validation.d.ts +28 -0
  208. package/dist/environment/validation.d.ts.map +1 -0
  209. package/dist/environment/validation.js +320 -0
  210. package/dist/environment/validation.js.map +1 -0
  211. package/dist/esm/accounts/accounts.js +1 -0
  212. package/dist/esm/accounts/azureManualOnboarding.js +8 -0
  213. package/dist/esm/accounts/azureSpSetup.js +14 -1
  214. package/dist/esm/accounts/azureSpSetupDurable.js +27 -1
  215. package/dist/esm/accounts/index.js +1 -0
  216. package/dist/esm/ai/index.js +10 -1
  217. package/dist/esm/aws/commitments-planning.js +5 -0
  218. package/dist/esm/aws/estates.js +7 -1
  219. package/dist/esm/aws/index.js +5 -0
  220. package/dist/esm/aws/organizationCommitments.js +48 -0
  221. package/dist/esm/aws/organizationCommitmentsApiValidation.js +220 -0
  222. package/dist/esm/aws/organizationCommitmentsValidation.js +224 -0
  223. package/dist/esm/aws/portalOrganizationCommitmentsPlanningPublicArtifactValidation.js +402 -0
  224. package/dist/esm/aws/portalOrganizationCommitmentsPlanningPublicArtifacts.js +2 -0
  225. package/dist/esm/aws/requests.js +1 -1
  226. package/dist/esm/azure/financialAnalyticsDelivery.js +2 -0
  227. package/dist/esm/azure/financialAnalyticsDisplayKernel.js +139 -0
  228. package/dist/esm/azure/financialAnalyticsValidation.js +61 -7
  229. package/dist/esm/azure/financialAuthorityProjectionKernel.js +159 -0
  230. package/dist/esm/azure/financialAuthorityViewValidation.js +204 -1
  231. package/dist/esm/azure/financialChargeComposition.js +36 -0
  232. package/dist/esm/azure/financialChargeCompositionKernel.js +48 -0
  233. package/dist/esm/azure/financialChargeCompositionValidation.js +155 -0
  234. package/dist/esm/azure/financialDataflowKernel.js +60 -7
  235. package/dist/esm/azure/financialDataflowValidation.js +146 -28
  236. package/dist/esm/azure/financialPolicyValidation.js +5 -0
  237. package/dist/esm/azure/financialProjectionValidation.js +43 -3
  238. package/dist/esm/azure/financialSavingsAuthorityValidation.js +124 -2
  239. package/dist/esm/azure/financialSavingsCoordinateValidation.js +62 -19
  240. package/dist/esm/azure/financialSavingsSurfaceProjection.js +2 -0
  241. package/dist/esm/azure/financialSavingsSurfaceProjectionKernel.js +318 -0
  242. package/dist/esm/azure/financialSavingsSurfaceProjectionValidation.js +126 -12
  243. package/dist/esm/azure/financialScope.js +6 -0
  244. package/dist/esm/azure/governance.js +1 -0
  245. package/dist/esm/azure/vmPatchManagement.js +3 -0
  246. package/dist/esm/entries/commitments-planning.js +2 -2
  247. package/dist/esm/entries/environment.js +3 -0
  248. package/dist/esm/environment/canonicalization.js +34 -0
  249. package/dist/esm/environment/contracts.js +17 -0
  250. package/dist/esm/environment/index.js +4 -0
  251. package/dist/esm/environment/internal.js +97 -0
  252. package/dist/esm/environment/references.js +194 -0
  253. package/dist/esm/environment/validation.js +303 -0
  254. package/dist/esm/index.js +2 -0
  255. package/dist/index.d.ts +2 -0
  256. package/dist/index.d.ts.map +1 -1
  257. package/dist/index.js +2 -0
  258. package/dist/index.js.map +1 -1
  259. package/dist/portfolio/portfolioOperations.d.ts +1 -1
  260. package/dist/portfolio/portfolioOperations.d.ts.map +1 -1
  261. package/package.json +15 -3
@@ -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,50 @@ 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 includedBaselineIds = new Set(members.flatMap(member => (member.status === 'included' ? [member.baselineId] : [])));
96
+ const includedBaselines = available.filter(baseline => includedBaselineIds.has(baseline.baselineId));
69
97
  const unavailableReasons = new Set(sortedBaselines
70
98
  .filter((baseline) => baseline.status === 'unavailable')
71
99
  .map(baseline => baseline.unavailableReason));
100
+ members.filter(member => member.status === 'unavailable').forEach(member => unavailableReasons.add(member.reasonCode));
101
+ const selectionReasonCodes = new Set();
102
+ selections.forEach(selection => selection.reasonCodes?.forEach(reason => {
103
+ selectionReasonCodes.add(reason);
104
+ unavailableReasons.add(reason);
105
+ }));
72
106
  if (available.some(baseline => !isCompleteFinancialBaselinePeriodV2(baseline.period)))
73
107
  unavailableReasons.add('coverage-incomplete');
74
108
  if (request.coordinate.accountingCurrency.status === 'unresolved') {
@@ -84,7 +118,7 @@ export const composeCurrentSpendV1 = (request) => {
84
118
  };
85
119
  const currency = request.coordinate.accountingCurrency.status === 'resolved' ? request.coordinate.accountingCurrency.currencyCode : undefined;
86
120
  let identity;
87
- if (!currency || available.length === 0) {
121
+ if (!currency || includedBaselines.length === 0) {
88
122
  identity = {
89
123
  ...common,
90
124
  amount: {
@@ -94,17 +128,36 @@ export const composeCurrentSpendV1 = (request) => {
94
128
  };
95
129
  }
96
130
  else {
97
- const knownAmount = formatExactDecimalValue(sumCanonicalDecimals(available.map(baseline => baseline.total.amount)));
131
+ const includedSelections = members.flatMap(member => member.status === 'included' ? [selections.get(member.baselineId)] : []);
132
+ const aggregateSelection = {
133
+ status: includedSelections.some(selection => selection.status === 'partial') ? 'partial' : 'available',
134
+ includedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.includedAmount))),
135
+ excludedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.excludedAmount))),
136
+ withheldAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.withheldAmount))),
137
+ forecastEligibleAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.forecastEligibleAmount))),
138
+ oneTimeAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.oneTimeAmount))),
139
+ unknownRecurrenceAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.unknownRecurrenceAmount))),
140
+ forecastStatus: includedSelections.some(selection => selection.forecastStatus === 'partial') ? 'partial' : 'available',
141
+ currencyCode: currency,
142
+ ...(selectionReasonCodes.size === 0 ? {} : { reasonCodes: uniqueSorted(selectionReasonCodes) }),
143
+ ...(includedSelections.every(selection => selection.forecastReasonCodes === undefined)
144
+ ? {}
145
+ : {
146
+ forecastReasonCodes: uniqueSorted(includedSelections.flatMap(selection => selection.forecastReasonCodes ?? [])),
147
+ }),
148
+ };
149
+ const knownAmount = aggregateSelection.includedAmount;
98
150
  identity =
99
151
  unavailableReasons.size === 0
100
- ? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency } }
152
+ ? { ...common, amount: { status: 'available', amount: knownAmount, currencyCode: currency }, chargeSelection: aggregateSelection }
101
153
  : {
102
154
  ...common,
103
155
  amount: { status: 'partial', knownAmount, currencyCode: currency, reasonCodes: uniqueSorted(unavailableReasons) },
156
+ chargeSelection: aggregateSelection,
104
157
  };
105
158
  }
106
159
  const composition = { ...identity, compositionId: createCurrentSpendCompositionIdV1(identity) };
107
- if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines)) {
160
+ if (!isCurrentSpendCompositionCompatibleV1(composition, sortedBaselines, request.chargeCompositions)) {
108
161
  throw new TypeError('Current-spend composition does not reconcile to its authoritative baseline members.');
109
162
  }
110
163
  return composition;
@@ -2,6 +2,8 @@ import { sha256Utf8 } from '../common/sha256.js';
2
2
  import { formatExactDecimalValue, sumCanonicalDecimals } from '../common/exactDecimal.js';
3
3
  import { toCanonicalEstimateLensV1 } from './costComposition.js';
4
4
  import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
5
+ import { AZURE_BILLED_ALL_CHARGES_POLICY_V1, resolveFinancialChargeInclusionPolicyV1, } from './financialChargeComposition.js';
6
+ import { isFinancialChargeCompositionV1, selectFinancialChargesV1 } from './financialChargeCompositionValidation.js';
5
7
  import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
6
8
  import { isFinancialScopeBaselineEnvelopeV2 } from './financialScopeBaselineValidation.js';
7
9
  import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
@@ -361,7 +363,18 @@ export const isFinancialDataflowScopeSelectorV1 = (value) => {
361
363
  };
362
364
  export const isFinancialDataflowCoordinateV1 = (value) => {
363
365
  if (!isFinancialDataflowRecordV1(value) ||
364
- !hasFinancialDataflowExactFieldsV1(value, ['companyId', 'provider', 'providerAccountRefs', 'scope', 'periodRole', 'period', 'costBasis', 'estimateLens', 'accountingCurrency'], ['requestedCurrencyCode']) ||
366
+ !hasFinancialDataflowExactFieldsV1(value, [
367
+ 'companyId',
368
+ 'provider',
369
+ 'providerAccountRefs',
370
+ 'scope',
371
+ 'periodRole',
372
+ 'period',
373
+ 'costBasis',
374
+ 'estimateLens',
375
+ 'accountingCurrency',
376
+ 'chargeInclusionPolicyRef',
377
+ ], ['requestedCurrencyCode']) ||
365
378
  !isFinancialDataflowIdentityV1(value.companyId) ||
366
379
  value.provider !== 'azure' ||
367
380
  !isFinancialDataflowSortedUniqueStringsV1(value.providerAccountRefs, FINANCIAL_DATAFLOW_LIMITS_V1.maximumProviderAccounts) ||
@@ -375,7 +388,12 @@ export const isFinancialDataflowCoordinateV1 = (value) => {
375
388
  typeof value.estimateLens !== 'string' ||
376
389
  !ESTIMATE_LENSES.has(value.estimateLens) ||
377
390
  (value.requestedCurrencyCode !== undefined && !isFinancialDataflowCurrencyV1(value.requestedCurrencyCode)) ||
378
- !isFinancialDataflowRecordV1(value.accountingCurrency)) {
391
+ !isFinancialDataflowRecordV1(value.accountingCurrency) ||
392
+ !isFinancialDataflowRecordV1(value.chargeInclusionPolicyRef) ||
393
+ !hasFinancialDataflowExactFieldsV1(value.chargeInclusionPolicyRef, ['policyId', 'policyDigest']) ||
394
+ !isFinancialDataflowIdentityV1(value.chargeInclusionPolicyRef.policyId) ||
395
+ !isFinancialDataflowHashV1(value.chargeInclusionPolicyRef.policyDigest) ||
396
+ resolveFinancialChargeInclusionPolicyV1(value.chargeInclusionPolicyRef) === undefined) {
379
397
  return false;
380
398
  }
381
399
  if (value.accountingCurrency.status === 'resolved') {
@@ -403,7 +421,9 @@ const isMember = (value) => {
403
421
  if (!isFinancialDataflowRecordV1(value) || !isFinancialDataflowIdentityV1(value.memberScopeId))
404
422
  return false;
405
423
  if (value.status === 'included') {
406
- return hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'baselineId', 'status']) && isFinancialDataflowHashV1(value.baselineId);
424
+ return (hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'baselineId', 'chargeCompositionId', 'status']) &&
425
+ isFinancialDataflowHashV1(value.baselineId) &&
426
+ isFinancialDataflowHashV1(value.chargeCompositionId));
407
427
  }
408
428
  return (value.status === 'unavailable' &&
409
429
  hasFinancialDataflowExactFieldsV1(value, ['memberScopeId', 'status', 'reasonCode']) &&
@@ -421,15 +441,7 @@ export const canonicalizeCurrentSpendMembershipV1 = (members) => {
421
441
  export const createCurrentSpendMembershipDigestV1 = (members) => `sha256:${sha256Utf8(canonicalizeCurrentSpendMembershipV1(members))}`;
422
442
  const isCompositionIdentity = (value) => {
423
443
  if (!isFinancialDataflowRecordV1(value) ||
424
- !hasFinancialDataflowExactFieldsV1(value, [
425
- 'schemaVersion',
426
- 'contractVersion',
427
- 'coordinate',
428
- 'members',
429
- 'amount',
430
- 'membershipDigest',
431
- 'algorithmVersion',
432
- ]) ||
444
+ !hasFinancialDataflowExactFieldsV1(value, ['schemaVersion', 'contractVersion', 'coordinate', 'members', 'amount', 'membershipDigest', 'algorithmVersion'], ['chargeSelection']) ||
433
445
  value.schemaVersion !== FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1 ||
434
446
  value.contractVersion !== FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1 ||
435
447
  !isFinancialDataflowCoordinateV1(value.coordinate) ||
@@ -445,11 +457,45 @@ const isCompositionIdentity = (value) => {
445
457
  return false;
446
458
  }
447
459
  const currency = value.coordinate.accountingCurrency.status === 'resolved' ? value.coordinate.accountingCurrency.currencyCode : undefined;
460
+ const isChargeSelection = (selection) => {
461
+ if (!isFinancialDataflowRecordV1(selection) ||
462
+ !hasFinancialDataflowExactFieldsV1(selection, [
463
+ 'status',
464
+ 'includedAmount',
465
+ 'excludedAmount',
466
+ 'withheldAmount',
467
+ 'forecastEligibleAmount',
468
+ 'oneTimeAmount',
469
+ 'unknownRecurrenceAmount',
470
+ 'forecastStatus',
471
+ 'currencyCode',
472
+ ], ['reasonCodes', 'forecastReasonCodes']) ||
473
+ (selection.status !== 'available' && selection.status !== 'partial') ||
474
+ (selection.forecastStatus !== 'available' && selection.forecastStatus !== 'partial') ||
475
+ !isFinancialDataflowCurrencyV1(selection.currencyCode) ||
476
+ ![
477
+ selection.includedAmount,
478
+ selection.excludedAmount,
479
+ selection.withheldAmount,
480
+ selection.forecastEligibleAmount,
481
+ selection.oneTimeAmount,
482
+ selection.unknownRecurrenceAmount,
483
+ ].every(amount => isCanonicalExactMoney({ amount, currencyCode: selection.currencyCode })))
484
+ return false;
485
+ return ((selection.status === 'available' ? selection.reasonCodes === undefined : isReasonCodes(selection.reasonCodes)) &&
486
+ (selection.forecastStatus === 'available'
487
+ ? selection.forecastReasonCodes === undefined
488
+ : isReasonCodes(selection.forecastReasonCodes)));
489
+ };
448
490
  if (value.amount.status === 'available') {
449
491
  return (currency !== undefined &&
450
492
  hasFinancialDataflowExactFieldsV1(value.amount, ['status', 'amount', 'currencyCode']) &&
451
493
  isCanonicalExactMoney({ amount: value.amount.amount, currencyCode: value.amount.currencyCode }) &&
452
494
  value.amount.currencyCode === currency &&
495
+ isChargeSelection(value.chargeSelection) &&
496
+ value.chargeSelection.status === 'available' &&
497
+ value.chargeSelection.currencyCode === currency &&
498
+ value.chargeSelection.includedAmount === value.amount.amount &&
453
499
  value.members.length > 0 &&
454
500
  value.members.every(member => member.status === 'included'));
455
501
  }
@@ -459,11 +505,17 @@ const isCompositionIdentity = (value) => {
459
505
  isCanonicalExactMoney({ amount: value.amount.knownAmount, currencyCode: value.amount.currencyCode }) &&
460
506
  value.amount.currencyCode === currency &&
461
507
  isReasonCodes(value.amount.reasonCodes) &&
508
+ isChargeSelection(value.chargeSelection) &&
509
+ value.chargeSelection.currencyCode === currency &&
510
+ value.chargeSelection.includedAmount === value.amount.knownAmount &&
462
511
  value.members.some(member => member.status === 'included') &&
463
- (value.members.some(member => member.status === 'unavailable') || value.amount.reasonCodes.includes('coverage-incomplete')));
512
+ (value.members.some(member => member.status === 'unavailable') ||
513
+ value.amount.reasonCodes.includes('coverage-incomplete') ||
514
+ value.chargeSelection.status === 'partial'));
464
515
  }
465
516
  const unresolvedCurrency = value.coordinate.accountingCurrency;
466
517
  return (value.amount.status === 'unavailable' &&
518
+ value.chargeSelection === undefined &&
467
519
  hasFinancialDataflowExactFieldsV1(value.amount, ['status', 'reasonCodes']) &&
468
520
  isReasonCodes(value.amount.reasonCodes) &&
469
521
  (currency !== undefined || (unresolvedCurrency.status === 'unresolved' && value.amount.reasonCodes.includes(unresolvedCurrency.reasonCode))));
@@ -480,6 +532,19 @@ export const canonicalizeCurrentSpendCompositionIdentityV1 = (value) => {
480
532
  },
481
533
  members: [...value.members].sort((left, right) => left.memberScopeId.localeCompare(right.memberScopeId)),
482
534
  amount: value.amount.status === 'available' ? value.amount : { ...value.amount, reasonCodes: [...value.amount.reasonCodes].sort() },
535
+ ...(value.chargeSelection === undefined
536
+ ? {}
537
+ : {
538
+ chargeSelection: {
539
+ ...value.chargeSelection,
540
+ ...(value.chargeSelection.reasonCodes === undefined
541
+ ? {}
542
+ : { reasonCodes: [...value.chargeSelection.reasonCodes].sort() }),
543
+ ...(value.chargeSelection.forecastReasonCodes === undefined
544
+ ? {}
545
+ : { forecastReasonCodes: [...value.chargeSelection.forecastReasonCodes].sort() }),
546
+ },
547
+ }),
483
548
  };
484
549
  return canonicalizeFinancialDataflowJsonV1(canonical);
485
550
  };
@@ -496,7 +561,7 @@ export const isCurrentSpendCompositionV1 = (value) => {
496
561
  'amount',
497
562
  'membershipDigest',
498
563
  'algorithmVersion',
499
- ])) {
564
+ ], ['chargeSelection'])) {
500
565
  return false;
501
566
  }
502
567
  const { compositionId, ...identity } = value;
@@ -511,65 +576,118 @@ const hasCompatibleBaselineCoordinate = (coordinate, baseline) => baseline.provi
511
576
  toCanonicalEstimateLensV1(baseline.estimateLens) === coordinate.estimateLens &&
512
577
  baseline.requestedCurrencyCode === coordinate.requestedCurrencyCode;
513
578
  /** Proves that composition membership and money reconcile to exact V2 baseline envelopes. */
514
- export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) => {
579
+ export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines, chargeCompositions) => {
515
580
  if (!isCurrentSpendCompositionV1(composition) ||
516
581
  !Array.isArray(baselines) ||
517
582
  baselines.length !== composition.members.length ||
518
583
  baselines.length > FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers ||
519
- !baselines.every(isFinancialScopeBaselineEnvelopeV2)) {
584
+ !baselines.every(isFinancialScopeBaselineEnvelopeV2) ||
585
+ !Array.isArray(chargeCompositions) ||
586
+ !chargeCompositions.every(isFinancialChargeCompositionV1)) {
520
587
  return false;
521
588
  }
522
589
  const typedBaselines = baselines;
590
+ const typedChargeCompositions = chargeCompositions;
591
+ const chargeCompositionById = new Map(typedChargeCompositions.map(value => [value.chargeCompositionId, value]));
592
+ if (chargeCompositionById.size !== typedChargeCompositions.length)
593
+ return false;
523
594
  const baselineByScopeId = new Map(typedBaselines.map(baseline => [baseline.scopeId, baseline]));
524
595
  if (baselineByScopeId.size !== typedBaselines.length ||
525
596
  new Set(typedBaselines.filter(baseline => baseline.status === 'available').map(baseline => baseline.baselineId)).size !==
526
597
  typedBaselines.filter(baseline => baseline.status === 'available').length) {
527
598
  return false;
528
599
  }
529
- const includedAmounts = [];
600
+ const includedSelections = [];
530
601
  const expectedReasonCodes = new Set();
531
602
  for (const member of composition.members) {
532
603
  const baseline = baselineByScopeId.get(member.memberScopeId);
533
604
  if (baseline === undefined || !hasCompatibleBaselineCoordinate(composition.coordinate, baseline))
534
605
  return false;
535
606
  if (member.status === 'included') {
536
- if (baseline.status !== 'available' || member.baselineId !== baseline.baselineId)
607
+ if (baseline.status !== 'available' ||
608
+ baseline.baselineKind !== 'owner' ||
609
+ member.baselineId !== baseline.baselineId ||
610
+ canonicalizeFinancialDataflowJsonV1(baseline.chargeInclusionPolicyRef) !==
611
+ canonicalizeFinancialDataflowJsonV1(AZURE_BILLED_ALL_CHARGES_POLICY_V1.policyRef))
612
+ return false;
613
+ const chargeComposition = chargeCompositionById.get(member.chargeCompositionId);
614
+ if (chargeComposition === undefined ||
615
+ chargeComposition.baselineId !== baseline.baselineId ||
616
+ chargeComposition.ownerScopeId !== baseline.scopeId ||
617
+ chargeComposition.period.windowKind !== baseline.period.windowKind ||
618
+ canonicalizeFinancialDataflowJsonV1(chargeComposition.period.requested) !== canonicalizeFinancialDataflowJsonV1(baseline.period.requested) ||
619
+ chargeComposition.costBasis !== baseline.costBasis ||
620
+ chargeComposition.estimateLens !== baseline.estimateLens ||
621
+ chargeComposition.accountingCurrencyCode !== baseline.total.currencyCode ||
622
+ chargeComposition.reconciliation.sourceTotal !== baseline.total.amount)
537
623
  return false;
538
624
  if (composition.coordinate.accountingCurrency.status === 'resolved' &&
539
625
  baseline.total.currencyCode !== composition.coordinate.accountingCurrency.currencyCode) {
540
626
  return false;
541
627
  }
542
- includedAmounts.push(baseline.total.amount);
628
+ includedSelections.push(selectFinancialChargesV1(chargeComposition, composition.coordinate.chargeInclusionPolicyRef));
629
+ includedSelections[includedSelections.length - 1].reasonCodes?.forEach(reason => expectedReasonCodes.add(reason));
543
630
  if (baseline.status === 'available' && !isCompleteFinancialBaselinePeriodV2(baseline.period)) {
544
631
  expectedReasonCodes.add('coverage-incomplete');
545
632
  }
546
633
  continue;
547
634
  }
548
- if (baseline.status !== 'unavailable' || member.reasonCode !== baseline.unavailableReason)
549
- return false;
550
- expectedReasonCodes.add(baseline.unavailableReason);
635
+ if (baseline.status === 'unavailable') {
636
+ if (member.reasonCode !== baseline.unavailableReason)
637
+ return false;
638
+ expectedReasonCodes.add(baseline.unavailableReason);
639
+ }
640
+ else {
641
+ const expectedDerivedReason = baseline.baselineKind === 'aggregate'
642
+ ? 'charge-composition-requires-owner-baseline'
643
+ : 'charge-composition-unavailable';
644
+ if (member.reasonCode !== expectedDerivedReason)
645
+ return false;
646
+ expectedReasonCodes.add(expectedDerivedReason);
647
+ }
551
648
  }
552
649
  if (composition.coordinate.accountingCurrency.status === 'unresolved') {
553
650
  expectedReasonCodes.add(composition.coordinate.accountingCurrency.reasonCode);
554
651
  }
652
+ if (typedChargeCompositions.length !== includedSelections.length)
653
+ return false;
555
654
  const hasExpectedReasonCodes = (reasonCodes) => reasonCodes.length === expectedReasonCodes.size && reasonCodes.every(reasonCode => expectedReasonCodes.has(reasonCode));
556
655
  if (composition.coordinate.accountingCurrency.status === 'unresolved') {
557
656
  return composition.amount.status === 'unavailable' && hasExpectedReasonCodes(composition.amount.reasonCodes);
558
657
  }
559
- if (includedAmounts.length === 0) {
658
+ if (includedSelections.length === 0) {
560
659
  return composition.amount.status === 'unavailable' && hasExpectedReasonCodes(composition.amount.reasonCodes);
561
660
  }
562
- let includedTotal;
661
+ let aggregateSelection;
563
662
  try {
564
- includedTotal = formatExactDecimalValue(sumCanonicalDecimals(includedAmounts));
663
+ const reasonCodes = new Set(includedSelections.flatMap(selection => selection.reasonCodes ?? []));
664
+ const forecastReasonCodes = new Set(includedSelections.flatMap(selection => selection.forecastReasonCodes ?? []));
665
+ aggregateSelection = {
666
+ status: includedSelections.some(selection => selection.status === 'partial') ? 'partial' : 'available',
667
+ includedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.includedAmount))),
668
+ excludedAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.excludedAmount))),
669
+ withheldAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.withheldAmount))),
670
+ forecastEligibleAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.forecastEligibleAmount))),
671
+ oneTimeAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.oneTimeAmount))),
672
+ unknownRecurrenceAmount: formatExactDecimalValue(sumCanonicalDecimals(includedSelections.map(selection => selection.unknownRecurrenceAmount))),
673
+ forecastStatus: includedSelections.some(selection => selection.forecastStatus === 'partial') ? 'partial' : 'available',
674
+ currencyCode: composition.coordinate.accountingCurrency.currencyCode,
675
+ ...(reasonCodes.size === 0 ? {} : { reasonCodes: [...reasonCodes].sort() }),
676
+ ...(forecastReasonCodes.size === 0
677
+ ? {}
678
+ : { forecastReasonCodes: [...forecastReasonCodes].sort() }),
679
+ };
565
680
  }
566
681
  catch {
567
682
  return false;
568
683
  }
569
- if (includedAmounts.length === composition.members.length && expectedReasonCodes.size === 0) {
570
- return composition.amount.status === 'available' && composition.amount.amount === includedTotal;
684
+ const sameSelection = canonicalizeFinancialDataflowJsonV1(composition.chargeSelection) === canonicalizeFinancialDataflowJsonV1(aggregateSelection);
685
+ if (!sameSelection)
686
+ return false;
687
+ if (includedSelections.length === composition.members.length && expectedReasonCodes.size === 0) {
688
+ return composition.amount.status === 'available' && composition.amount.amount === aggregateSelection.includedAmount;
571
689
  }
572
690
  return (composition.amount.status === 'partial' &&
573
- composition.amount.knownAmount === includedTotal &&
691
+ composition.amount.knownAmount === aggregateSelection.includedAmount &&
574
692
  hasExpectedReasonCodes(composition.amount.reasonCodes));
575
693
  };
@@ -1,6 +1,7 @@
1
1
  import { multiplyExactDecimalValues, parseCanonicalDecimal, subtractExactDecimalValues } from '../common/exactDecimal.js';
2
2
  import { sha256Utf8 } from '../common/sha256.js';
3
3
  import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
4
+ import { isFinancialChargeInclusionPolicyRefV1 } from './financialChargeCompositionValidation.js';
4
5
  import { FINANCIAL_POLICY_DEFINITION_CONTRACT_VERSION_V1, FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1, } from './financialPolicy.js';
5
6
  import { FINANCIAL_DATAFLOW_LIMITS_V1, canonicalizeFinancialDataflowJsonV1, canonicalizeFinancialDataflowCoordinateV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowCurrencyV1, createFinancialDataflowCoordinateIdV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowIsoInstantV1, isFinancialDataflowRecordV1, isFinancialDataflowScopeSelectorV1, isFinancialDataflowSortedUniqueStringsV1, isFinancialDataflowValueWithinLimitsV1, } from './financialDataflowValidation.js';
6
7
  import { isCurrentSpendCompositionV1 } from './financialDataflowValidation.js';
@@ -85,6 +86,7 @@ const isCoordinateRequest = (value, companyId) => isFinancialDataflowRecordV1(va
85
86
  'period',
86
87
  'costBasis',
87
88
  'estimateLens',
89
+ 'chargeInclusionPolicyRef',
88
90
  'requiredAccountingCurrencyCode',
89
91
  ]) &&
90
92
  value.provider === 'azure' &&
@@ -107,6 +109,7 @@ const isCoordinateRequest = (value, companyId) => isFinancialDataflowRecordV1(va
107
109
  COST_BASES.has(value.costBasis) &&
108
110
  typeof value.estimateLens === 'string' &&
109
111
  ESTIMATE_LENSES.has(value.estimateLens) &&
112
+ isFinancialChargeInclusionPolicyRefV1(value.chargeInclusionPolicyRef) &&
110
113
  isFinancialDataflowCurrencyV1(value.requiredAccountingCurrencyCode) &&
111
114
  isFinancialDataflowIdentityV1(companyId);
112
115
  const isCriteria = (value, currencyCode) => {
@@ -418,6 +421,8 @@ export const isFinancialPolicyEvaluationCompatibleV1 = (evaluation, definition,
418
421
  request.scope.scopeId !== coordinate.scope.scopeId ||
419
422
  request.costBasis !== coordinate.costBasis ||
420
423
  request.estimateLens !== coordinate.estimateLens ||
424
+ canonicalizeFinancialDataflowJsonV1(request.chargeInclusionPolicyRef) !==
425
+ canonicalizeFinancialDataflowJsonV1(coordinate.chargeInclusionPolicyRef) ||
421
426
  coordinate.requestedCurrencyCode !== request.requiredAccountingCurrencyCode ||
422
427
  (currency === undefined
423
428
  ? currentSpendComposition.amount.status !== 'unavailable'