@pagopa/dx-savemoney 0.3.3 → 0.5.0

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 (174) hide show
  1. package/README.md +12 -0
  2. package/dist/__tests__/finding.test.js +22 -0
  3. package/dist/__tests__/finding.test.js.map +1 -1
  4. package/dist/azure/__tests__/analyzer-tags.test.js +28 -1
  5. package/dist/azure/__tests__/analyzer-tags.test.js.map +1 -1
  6. package/dist/azure/__tests__/config.test.js +31 -1
  7. package/dist/azure/__tests__/config.test.js.map +1 -1
  8. package/dist/azure/__tests__/report.test.js +71 -0
  9. package/dist/azure/__tests__/report.test.js.map +1 -1
  10. package/dist/azure/analyzer.d.ts +8 -1
  11. package/dist/azure/analyzer.d.ts.map +1 -1
  12. package/dist/azure/analyzer.js +57 -41
  13. package/dist/azure/analyzer.js.map +1 -1
  14. package/dist/azure/analyzers/registry.d.ts.map +1 -1
  15. package/dist/azure/analyzers/registry.js +4 -4
  16. package/dist/azure/analyzers/registry.js.map +1 -1
  17. package/dist/azure/analyzers/types.d.ts +6 -0
  18. package/dist/azure/analyzers/types.d.ts.map +1 -1
  19. package/dist/azure/config.d.ts.map +1 -1
  20. package/dist/azure/config.js +1 -5
  21. package/dist/azure/config.js.map +1 -1
  22. package/dist/azure/pricing/__tests__/cache.test.d.ts +8 -0
  23. package/dist/azure/pricing/__tests__/cache.test.d.ts.map +1 -0
  24. package/dist/azure/pricing/__tests__/cache.test.js +56 -0
  25. package/dist/azure/pricing/__tests__/cache.test.js.map +1 -0
  26. package/dist/azure/pricing/__tests__/client.test.d.ts +9 -0
  27. package/dist/azure/pricing/__tests__/client.test.d.ts.map +1 -0
  28. package/dist/azure/pricing/__tests__/client.test.js +181 -0
  29. package/dist/azure/pricing/__tests__/client.test.js.map +1 -0
  30. package/dist/azure/pricing/__tests__/pricing-service.test.d.ts +6 -0
  31. package/dist/azure/pricing/__tests__/pricing-service.test.d.ts.map +1 -0
  32. package/dist/azure/pricing/__tests__/pricing-service.test.js +283 -0
  33. package/dist/azure/pricing/__tests__/pricing-service.test.js.map +1 -0
  34. package/dist/azure/pricing/cache.d.ts +45 -0
  35. package/dist/azure/pricing/cache.d.ts.map +1 -0
  36. package/dist/azure/pricing/cache.js +79 -0
  37. package/dist/azure/pricing/cache.js.map +1 -0
  38. package/dist/azure/pricing/client.d.ts +67 -0
  39. package/dist/azure/pricing/client.d.ts.map +1 -0
  40. package/dist/azure/pricing/client.js +96 -0
  41. package/dist/azure/pricing/client.js.map +1 -0
  42. package/dist/azure/pricing/index.d.ts +6 -0
  43. package/dist/azure/pricing/index.d.ts.map +1 -0
  44. package/dist/azure/pricing/index.js +6 -0
  45. package/dist/azure/pricing/index.js.map +1 -0
  46. package/dist/azure/pricing/pricing-service.d.ts +46 -0
  47. package/dist/azure/pricing/pricing-service.d.ts.map +1 -0
  48. package/dist/azure/pricing/pricing-service.js +84 -0
  49. package/dist/azure/pricing/pricing-service.js.map +1 -0
  50. package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts +5 -0
  51. package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts.map +1 -0
  52. package/dist/azure/pricing/resolvers/__tests__/app-service.test.js +137 -0
  53. package/dist/azure/pricing/resolvers/__tests__/app-service.test.js.map +1 -0
  54. package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts +5 -0
  55. package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts.map +1 -0
  56. package/dist/azure/pricing/resolvers/__tests__/disk.test.js +160 -0
  57. package/dist/azure/pricing/resolvers/__tests__/disk.test.js.map +1 -0
  58. package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts +5 -0
  59. package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts.map +1 -0
  60. package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js +113 -0
  61. package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js.map +1 -0
  62. package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts +9 -0
  63. package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts.map +1 -0
  64. package/dist/azure/pricing/resolvers/__tests__/vm.test.js +204 -0
  65. package/dist/azure/pricing/resolvers/__tests__/vm.test.js.map +1 -0
  66. package/dist/azure/pricing/resolvers/app-service.d.ts +27 -0
  67. package/dist/azure/pricing/resolvers/app-service.d.ts.map +1 -0
  68. package/dist/azure/pricing/resolvers/app-service.js +67 -0
  69. package/dist/azure/pricing/resolvers/app-service.js.map +1 -0
  70. package/dist/azure/pricing/resolvers/disk.d.ts +59 -0
  71. package/dist/azure/pricing/resolvers/disk.d.ts.map +1 -0
  72. package/dist/azure/pricing/resolvers/disk.js +140 -0
  73. package/dist/azure/pricing/resolvers/disk.js.map +1 -0
  74. package/dist/azure/pricing/resolvers/index.d.ts +12 -0
  75. package/dist/azure/pricing/resolvers/index.d.ts.map +1 -0
  76. package/dist/azure/pricing/resolvers/index.js +8 -0
  77. package/dist/azure/pricing/resolvers/index.js.map +1 -0
  78. package/dist/azure/pricing/resolvers/public-ip.d.ts +32 -0
  79. package/dist/azure/pricing/resolvers/public-ip.d.ts.map +1 -0
  80. package/dist/azure/pricing/resolvers/public-ip.js +56 -0
  81. package/dist/azure/pricing/resolvers/public-ip.js.map +1 -0
  82. package/dist/azure/pricing/resolvers/vm.d.ts +51 -0
  83. package/dist/azure/pricing/resolvers/vm.d.ts.map +1 -0
  84. package/dist/azure/pricing/resolvers/vm.js +89 -0
  85. package/dist/azure/pricing/resolvers/vm.js.map +1 -0
  86. package/dist/azure/pricing/schema.d.ts +88 -0
  87. package/dist/azure/pricing/schema.d.ts.map +1 -0
  88. package/dist/azure/pricing/schema.js +64 -0
  89. package/dist/azure/pricing/schema.js.map +1 -0
  90. package/dist/azure/pricing/test-cache-dir.d.ts +3 -0
  91. package/dist/azure/pricing/test-cache-dir.d.ts.map +1 -0
  92. package/dist/azure/pricing/test-cache-dir.js +17 -0
  93. package/dist/azure/pricing/test-cache-dir.js.map +1 -0
  94. package/dist/azure/report.d.ts.map +1 -1
  95. package/dist/azure/report.js +57 -18
  96. package/dist/azure/report.js.map +1 -1
  97. package/dist/azure/resources/__tests__/app-service.test.d.ts +5 -0
  98. package/dist/azure/resources/__tests__/app-service.test.d.ts.map +1 -0
  99. package/dist/azure/resources/__tests__/app-service.test.js +105 -0
  100. package/dist/azure/resources/__tests__/app-service.test.js.map +1 -0
  101. package/dist/azure/resources/app-service.d.ts +8 -3
  102. package/dist/azure/resources/app-service.d.ts.map +1 -1
  103. package/dist/azure/resources/app-service.js +35 -2
  104. package/dist/azure/resources/app-service.js.map +1 -1
  105. package/dist/azure/resources/disk.d.ts +3 -1
  106. package/dist/azure/resources/disk.d.ts.map +1 -1
  107. package/dist/azure/resources/disk.js +35 -3
  108. package/dist/azure/resources/disk.js.map +1 -1
  109. package/dist/azure/resources/public-ip.d.ts +3 -1
  110. package/dist/azure/resources/public-ip.d.ts.map +1 -1
  111. package/dist/azure/resources/public-ip.js +41 -3
  112. package/dist/azure/resources/public-ip.js.map +1 -1
  113. package/dist/azure/resources/vm.d.ts +4 -1
  114. package/dist/azure/resources/vm.d.ts.map +1 -1
  115. package/dist/azure/resources/vm.js +49 -6
  116. package/dist/azure/resources/vm.js.map +1 -1
  117. package/dist/azure/types.d.ts +18 -3
  118. package/dist/azure/types.d.ts.map +1 -1
  119. package/dist/finding.d.ts +3 -3
  120. package/dist/finding.d.ts.map +1 -1
  121. package/dist/finding.js +7 -0
  122. package/dist/finding.js.map +1 -1
  123. package/dist/index.d.ts +3 -2
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +1 -0
  126. package/dist/index.js.map +1 -1
  127. package/dist/schema.d.ts +5 -1
  128. package/dist/schema.d.ts.map +1 -1
  129. package/dist/schema.js +5 -3
  130. package/dist/schema.js.map +1 -1
  131. package/dist/types.d.ts +8 -0
  132. package/dist/types.d.ts.map +1 -1
  133. package/dist/types.js +2 -0
  134. package/dist/types.js.map +1 -1
  135. package/package.json +1 -1
  136. package/src/__tests__/finding.test.ts +24 -0
  137. package/src/azure/__tests__/analyzer-tags.test.ts +36 -1
  138. package/src/azure/__tests__/config.test.ts +43 -1
  139. package/src/azure/__tests__/fixtures/full-override.yaml +2 -0
  140. package/src/azure/__tests__/report.test.ts +78 -0
  141. package/src/azure/analyzer.ts +70 -52
  142. package/src/azure/analyzers/registry.ts +8 -2
  143. package/src/azure/analyzers/types.ts +6 -0
  144. package/src/azure/config.ts +1 -5
  145. package/src/azure/pricing/__tests__/cache.test.ts +67 -0
  146. package/src/azure/pricing/__tests__/client.test.ts +212 -0
  147. package/src/azure/pricing/__tests__/fixtures/vm-b2s-westeurope.json +51 -0
  148. package/src/azure/pricing/__tests__/pricing-service.test.ts +315 -0
  149. package/src/azure/pricing/cache.ts +92 -0
  150. package/src/azure/pricing/client.ts +137 -0
  151. package/src/azure/pricing/index.ts +6 -0
  152. package/src/azure/pricing/pricing-service.ts +125 -0
  153. package/src/azure/pricing/resolvers/__tests__/app-service.test.ts +154 -0
  154. package/src/azure/pricing/resolvers/__tests__/disk.test.ts +189 -0
  155. package/src/azure/pricing/resolvers/__tests__/public-ip.test.ts +130 -0
  156. package/src/azure/pricing/resolvers/__tests__/vm.test.ts +232 -0
  157. package/src/azure/pricing/resolvers/app-service.ts +102 -0
  158. package/src/azure/pricing/resolvers/disk.ts +170 -0
  159. package/src/azure/pricing/resolvers/index.ts +31 -0
  160. package/src/azure/pricing/resolvers/public-ip.ts +87 -0
  161. package/src/azure/pricing/resolvers/vm.ts +125 -0
  162. package/src/azure/pricing/schema.ts +69 -0
  163. package/src/azure/pricing/test-cache-dir.ts +26 -0
  164. package/src/azure/report.ts +86 -21
  165. package/src/azure/resources/__tests__/app-service.test.ts +165 -0
  166. package/src/azure/resources/app-service.ts +65 -4
  167. package/src/azure/resources/disk.ts +46 -4
  168. package/src/azure/resources/public-ip.ts +61 -2
  169. package/src/azure/resources/vm.ts +89 -11
  170. package/src/azure/types.ts +19 -3
  171. package/src/finding.ts +10 -3
  172. package/src/index.ts +4 -3
  173. package/src/schema.ts +7 -3
  174. package/src/types.ts +12 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * High-level facade that exposes per-resource pricing resolvers backed by
3
+ * a shared `PricingClient` (and therefore a shared disk cache).
4
+ *
5
+ * Two responsibilities:
6
+ * 1. Dispatch the right resolver for the right resource kind.
7
+ * 2. Memoize identical lookups within a single run so concurrent
8
+ * analyzers asking for the same `(sku, region, …)` only hit the
9
+ * network (or disk cache) once.
10
+ * 3. Swallow resolver errors and return `undefined` — the savings
11
+ * estimate is best-effort and should never break the main analysis
12
+ * flow.
13
+ */
14
+ import type { Money } from "../../finding.js";
15
+ import type { PricingClient } from "./client.js";
16
+ import type { ResolveAppServicePlanInput, ResolveDiskInput, ResolvePublicIpInput, ResolveVmInput } from "./resolvers/index.js";
17
+ export declare class PricingService {
18
+ private readonly client;
19
+ /** In-memory memo keyed by the canonicalised lookup input. */
20
+ private readonly memo;
21
+ constructor(client: PricingClient);
22
+ /**
23
+ * Resolves the estimated monthly cost of an App Service Plan SKU.
24
+ * Returns `undefined` on no-match or on error.
25
+ */
26
+ resolveAppServicePlan(input: ResolveAppServicePlanInput): Promise<Money | undefined>;
27
+ /**
28
+ * Resolves the estimated monthly cost of a Managed Disk. Returns
29
+ * `undefined` on no-match (e.g. unsupported SKU like UltraSSD) or on
30
+ * error.
31
+ */
32
+ resolveDisk(input: ResolveDiskInput): Promise<Money | undefined>;
33
+ /**
34
+ * Resolves the estimated monthly cost of a Public IP. Returns
35
+ * `undefined` on no-match or on error.
36
+ */
37
+ resolvePublicIp(input: ResolvePublicIpInput): Promise<Money | undefined>;
38
+ /**
39
+ * Resolves the estimated monthly cost of a Virtual Machine SKU.
40
+ * Returns `undefined` on no-match or on error.
41
+ */
42
+ resolveVm(input: ResolveVmInput): Promise<Money | undefined>;
43
+ private memoize;
44
+ private safeResolve;
45
+ }
46
+ //# sourceMappingURL=pricing-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing-service.d.ts","sourceRoot":"","sources":["../../../src/azure/pricing/pricing-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EACV,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAY9B,qBAAa,cAAc;IAIb,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiD;gBAEzC,MAAM,EAAE,aAAa;IAElD;;;OAGG;IACG,qBAAqB,CACzB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAS7B;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAOtE;;;OAGG;IACG,eAAe,CACnB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAO7B;;;OAGG;IACG,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAOlE,OAAO,CAAC,OAAO;YAoBD,WAAW;CAY1B"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * High-level facade that exposes per-resource pricing resolvers backed by
3
+ * a shared `PricingClient` (and therefore a shared disk cache).
4
+ *
5
+ * Two responsibilities:
6
+ * 1. Dispatch the right resolver for the right resource kind.
7
+ * 2. Memoize identical lookups within a single run so concurrent
8
+ * analyzers asking for the same `(sku, region, …)` only hit the
9
+ * network (or disk cache) once.
10
+ * 3. Swallow resolver errors and return `undefined` — the savings
11
+ * estimate is best-effort and should never break the main analysis
12
+ * flow.
13
+ */
14
+ import { getLogger } from "@logtape/logtape";
15
+ import { normalizeArmRegion, resolveAppServicePlanMonthlyPrice, resolveDiskMonthlyPrice, resolvePublicIpMonthlyPrice, resolveVmMonthlyPrice, } from "./resolvers/index.js";
16
+ const logger = getLogger(["savemoney", "azure", "pricing"]);
17
+ export class PricingService {
18
+ client;
19
+ /** In-memory memo keyed by the canonicalised lookup input. */
20
+ memo = new Map();
21
+ constructor(client) {
22
+ this.client = client;
23
+ }
24
+ /**
25
+ * Resolves the estimated monthly cost of an App Service Plan SKU.
26
+ * Returns `undefined` on no-match or on error.
27
+ */
28
+ async resolveAppServicePlan(input) {
29
+ const key = `app-service-plan|${normalizeArmRegion(input.armRegionName)}|${input.skuName}|${input.os ?? "any"}|${Math.ceil(input.workerCount ?? 1)}`;
30
+ return this.memoize(key, () => this.safeResolve(() => resolveAppServicePlanMonthlyPrice(this.client, input)));
31
+ }
32
+ /**
33
+ * Resolves the estimated monthly cost of a Managed Disk. Returns
34
+ * `undefined` on no-match (e.g. unsupported SKU like UltraSSD) or on
35
+ * error.
36
+ */
37
+ async resolveDisk(input) {
38
+ const key = `disk|${normalizeArmRegion(input.armRegionName)}|${input.sku}|${input.diskSizeGiB}`;
39
+ return this.memoize(key, () => this.safeResolve(() => resolveDiskMonthlyPrice(this.client, input)));
40
+ }
41
+ /**
42
+ * Resolves the estimated monthly cost of a Public IP. Returns
43
+ * `undefined` on no-match or on error.
44
+ */
45
+ async resolvePublicIp(input) {
46
+ const key = `public-ip|${normalizeArmRegion(input.armRegionName)}|${input.sku}|${input.allocation}`;
47
+ return this.memoize(key, () => this.safeResolve(() => resolvePublicIpMonthlyPrice(this.client, input)));
48
+ }
49
+ /**
50
+ * Resolves the estimated monthly cost of a Virtual Machine SKU.
51
+ * Returns `undefined` on no-match or on error.
52
+ */
53
+ async resolveVm(input) {
54
+ const key = `vm|${normalizeArmRegion(input.armRegionName)}|${input.armSkuName}|${input.os ?? "linux"}`;
55
+ return this.memoize(key, () => this.safeResolve(() => resolveVmMonthlyPrice(this.client, input)));
56
+ }
57
+ memoize(key, factory) {
58
+ const cached = this.memo.get(key);
59
+ if (cached)
60
+ return cached;
61
+ const promise = factory().then((money) => {
62
+ if (money === undefined) {
63
+ // Debug-level so it shows up under `-v` but doesn't clutter the
64
+ // default output. Helps diagnosing "I see no savings" reports
65
+ // when the Retail Prices API has no entry for the queried
66
+ // (region, sku, …) combination.
67
+ logger.debug(`Pricing lookup returned no match for ${key}`);
68
+ }
69
+ return money;
70
+ });
71
+ this.memo.set(key, promise);
72
+ return promise;
73
+ }
74
+ async safeResolve(factory) {
75
+ try {
76
+ return await factory();
77
+ }
78
+ catch (error) {
79
+ logger.warn(`Pricing lookup failed: ${error instanceof Error ? error.message : String(error)}`);
80
+ return undefined;
81
+ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=pricing-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing-service.js","sourceRoot":"","sources":["../../../src/azure/pricing/pricing-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for the App Service Plan Retail Prices resolver.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=app-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-service.test.d.ts","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/app-service.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Tests for the App Service Plan Retail Prices resolver.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { DiskCache } from "../../cache.js";
6
+ import { PricingClient } from "../../client.js";
7
+ import { makeTestCacheDir, removeTestCacheDir } from "../../test-cache-dir.js";
8
+ import { resolveAppServicePlanMonthlyPrice } from "../app-service.js";
9
+ import { HOURS_PER_MONTH } from "../vm.js";
10
+ function makeFetch(body) {
11
+ return vi.fn().mockResolvedValue({
12
+ json: async () => body,
13
+ ok: true,
14
+ status: 200,
15
+ statusText: "OK",
16
+ });
17
+ }
18
+ let dir;
19
+ beforeEach(async () => {
20
+ dir = await makeTestCacheDir("app-service-resolver");
21
+ });
22
+ afterEach(async () => {
23
+ await removeTestCacheDir(dir);
24
+ });
25
+ describe("resolveAppServicePlanMonthlyPrice", () => {
26
+ it("returns the monthly cost for the matching Linux plan SKU and worker count", async () => {
27
+ const body = {
28
+ Items: [
29
+ {
30
+ armRegionName: "westeurope",
31
+ currencyCode: "EUR",
32
+ meterName: "S1 App",
33
+ productName: "Azure App Service Standard Plan",
34
+ retailPrice: 0.087758,
35
+ skuName: "S1",
36
+ type: "Consumption",
37
+ unitOfMeasure: "1 Hour",
38
+ unitPrice: 0.087758,
39
+ },
40
+ {
41
+ armRegionName: "westeurope",
42
+ currencyCode: "EUR",
43
+ meterName: "S1 App",
44
+ productName: "Azure App Service Standard Plan - Linux",
45
+ retailPrice: 0.08337,
46
+ skuName: "S1",
47
+ type: "Consumption",
48
+ unitOfMeasure: "1 Hour",
49
+ unitPrice: 0.08337,
50
+ },
51
+ ],
52
+ NextPageLink: null,
53
+ };
54
+ const client = new PricingClient({
55
+ cache: new DiskCache({ dir }),
56
+ fetch: makeFetch(body),
57
+ });
58
+ const money = await resolveAppServicePlanMonthlyPrice(client, {
59
+ armRegionName: "westeurope",
60
+ os: "linux",
61
+ skuName: "S1",
62
+ workerCount: 2,
63
+ });
64
+ expect(money).toEqual({
65
+ amount: Math.round(0.08337 * HOURS_PER_MONTH * 2 * 100) / 100,
66
+ currency: "EUR",
67
+ });
68
+ });
69
+ it("matches Retail Prices SKU names with inconsistent spacing", async () => {
70
+ const body = {
71
+ Items: [
72
+ {
73
+ armRegionName: "westeurope",
74
+ currencyCode: "EUR",
75
+ meterName: "P2 v3 App",
76
+ productName: "Azure App Service Premium v3 Plan",
77
+ retailPrice: 0.593243,
78
+ skuName: "P2 v3",
79
+ type: "Consumption",
80
+ unitOfMeasure: "1 Hour",
81
+ unitPrice: 0.593243,
82
+ },
83
+ ],
84
+ NextPageLink: null,
85
+ };
86
+ const client = new PricingClient({
87
+ cache: new DiskCache({ dir }),
88
+ fetch: makeFetch(body),
89
+ });
90
+ const money = await resolveAppServicePlanMonthlyPrice(client, {
91
+ armRegionName: "West Europe",
92
+ os: "windows",
93
+ skuName: "P2v3",
94
+ });
95
+ expect(money?.amount).toBeCloseTo(0.593243 * HOURS_PER_MONTH, 2);
96
+ });
97
+ it("ignores non-plan and non-consumption meters", async () => {
98
+ const body = {
99
+ Items: [
100
+ {
101
+ armRegionName: "westeurope",
102
+ currencyCode: "EUR",
103
+ meterName: "Standard Bandwidth Usage",
104
+ productName: "Static Web Apps",
105
+ retailPrice: 0.175516,
106
+ skuName: "Standard",
107
+ type: "Consumption",
108
+ unitOfMeasure: "1 GB",
109
+ unitPrice: 0.175516,
110
+ },
111
+ {
112
+ armRegionName: "westeurope",
113
+ currencyCode: "EUR",
114
+ meterName: "S1 App",
115
+ productName: "Azure App Service Standard Plan",
116
+ reservationTerm: "1 Year",
117
+ retailPrice: 640,
118
+ skuName: "S1",
119
+ type: "Reservation",
120
+ unitOfMeasure: "1 Hour",
121
+ unitPrice: 640,
122
+ },
123
+ ],
124
+ NextPageLink: null,
125
+ };
126
+ const client = new PricingClient({
127
+ cache: new DiskCache({ dir }),
128
+ fetch: makeFetch(body),
129
+ });
130
+ const money = await resolveAppServicePlanMonthlyPrice(client, {
131
+ armRegionName: "westeurope",
132
+ skuName: "S1",
133
+ });
134
+ expect(money).toBeUndefined();
135
+ });
136
+ });
137
+ //# sourceMappingURL=app-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-service.test.js","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/app-service.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for the Managed Disk Retail Prices resolver.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=disk.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.test.d.ts","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/disk.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Tests for the Managed Disk Retail Prices resolver.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { DiskCache } from "../../cache.js";
6
+ import { PricingClient } from "../../client.js";
7
+ import { makeTestCacheDir, removeTestCacheDir } from "../../test-cache-dir.js";
8
+ import { pickTier, resolveDiskMonthlyPrice } from "../disk.js";
9
+ function makeFetch(body) {
10
+ const spy = vi.fn().mockResolvedValue({
11
+ json: async () => body,
12
+ ok: true,
13
+ status: 200,
14
+ statusText: "OK",
15
+ });
16
+ return { fetch: spy, spy };
17
+ }
18
+ let dir;
19
+ beforeEach(async () => {
20
+ dir = await makeTestCacheDir("disk-resolver");
21
+ });
22
+ afterEach(async () => {
23
+ await removeTestCacheDir(dir);
24
+ });
25
+ describe("pickTier", () => {
26
+ it.each([
27
+ [1, 1],
28
+ [4, 1],
29
+ [5, 2],
30
+ [128, 10],
31
+ [129, 15],
32
+ [1024, 30],
33
+ [32768, 80],
34
+ ])("maps %i GiB to tier %i", (sizeGiB, tier) => {
35
+ expect(pickTier(sizeGiB)).toBe(tier);
36
+ });
37
+ it("returns undefined for sizes above the largest tier", () => {
38
+ expect(pickTier(32769)).toBeUndefined();
39
+ });
40
+ });
41
+ describe("resolveDiskMonthlyPrice", () => {
42
+ it("resolves the monthly price for a Premium_LRS P10 disk", async () => {
43
+ const body = {
44
+ Items: [
45
+ {
46
+ armRegionName: "westeurope",
47
+ currencyCode: "EUR",
48
+ meterName: "P10 LRS Disk",
49
+ productName: "Premium SSD Managed Disks",
50
+ retailPrice: 19.71,
51
+ skuName: "P10 LRS",
52
+ type: "Consumption",
53
+ unitOfMeasure: "1/Month",
54
+ unitPrice: 19.71,
55
+ },
56
+ ],
57
+ NextPageLink: null,
58
+ };
59
+ const { fetch, spy } = makeFetch(body);
60
+ const client = new PricingClient({
61
+ cache: new DiskCache({ dir }),
62
+ fetch,
63
+ });
64
+ const money = await resolveDiskMonthlyPrice(client, {
65
+ armRegionName: "westeurope",
66
+ diskSizeGiB: 128,
67
+ sku: "Premium_LRS",
68
+ });
69
+ expect(money).toEqual({ amount: 19.71, currency: "EUR" });
70
+ expect(spy).toHaveBeenCalledTimes(1);
71
+ // The OData filter must target the tier-derived skuName, not the raw
72
+ // managed-disk SKU. URLSearchParams encodes spaces as `+`.
73
+ const url = String(spy.mock.calls[0]?.[0]);
74
+ expect(url).toContain("skuName+eq+%27P10+LRS%27");
75
+ expect(url).toContain("productName+eq+%27Premium+SSD+Managed+Disks%27");
76
+ });
77
+ it("uses the ZRS product/replication for Premium_ZRS", async () => {
78
+ const body = {
79
+ Items: [
80
+ {
81
+ armRegionName: "westeurope",
82
+ currencyCode: "EUR",
83
+ meterName: "P10 ZRS Disk",
84
+ productName: "Premium SSD Managed Disks",
85
+ retailPrice: 24.64,
86
+ skuName: "P10 ZRS",
87
+ type: "Consumption",
88
+ unitOfMeasure: "1/Month",
89
+ unitPrice: 24.64,
90
+ },
91
+ ],
92
+ NextPageLink: null,
93
+ };
94
+ const { fetch, spy } = makeFetch(body);
95
+ const client = new PricingClient({
96
+ cache: new DiskCache({ dir }),
97
+ fetch,
98
+ });
99
+ const money = await resolveDiskMonthlyPrice(client, {
100
+ armRegionName: "westeurope",
101
+ diskSizeGiB: 128,
102
+ sku: "Premium_ZRS",
103
+ });
104
+ expect(money).toEqual({ amount: 24.64, currency: "EUR" });
105
+ const url = String(spy.mock.calls[0]?.[0]);
106
+ expect(url).toContain("skuName+eq+%27P10+ZRS%27");
107
+ });
108
+ it("returns undefined when the response has no matching entry", async () => {
109
+ const body = {
110
+ Items: [
111
+ {
112
+ armRegionName: "westeurope",
113
+ currencyCode: "EUR",
114
+ // Per-GB-month item; resolver must skip it because of unitOfMeasure.
115
+ productName: "Premium SSD Managed Disks",
116
+ retailPrice: 0.1,
117
+ skuName: "P10 LRS",
118
+ type: "Consumption",
119
+ unitOfMeasure: "1 GB/Month",
120
+ unitPrice: 0.1,
121
+ },
122
+ ],
123
+ NextPageLink: null,
124
+ };
125
+ const client = new PricingClient({
126
+ cache: new DiskCache({ dir }),
127
+ fetch: makeFetch(body).fetch,
128
+ });
129
+ const money = await resolveDiskMonthlyPrice(client, {
130
+ armRegionName: "westeurope",
131
+ diskSizeGiB: 128,
132
+ sku: "Premium_LRS",
133
+ });
134
+ expect(money).toBeUndefined();
135
+ });
136
+ it("returns undefined when the disk size exceeds the largest tier", async () => {
137
+ const client = new PricingClient({
138
+ cache: new DiskCache({ dir }),
139
+ fetch: vi.fn(),
140
+ });
141
+ const money = await resolveDiskMonthlyPrice(client, {
142
+ armRegionName: "westeurope",
143
+ diskSizeGiB: 100_000,
144
+ sku: "Premium_LRS",
145
+ });
146
+ expect(money).toBeUndefined();
147
+ });
148
+ it("rejects invalid input via zod", async () => {
149
+ const client = new PricingClient({
150
+ cache: new DiskCache({ dir }),
151
+ fetch: vi.fn(),
152
+ });
153
+ await expect(resolveDiskMonthlyPrice(client, {
154
+ armRegionName: "westeurope",
155
+ diskSizeGiB: -1,
156
+ sku: "Premium_LRS",
157
+ })).rejects.toThrow();
158
+ });
159
+ });
160
+ //# sourceMappingURL=disk.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.test.js","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/disk.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for the Public IP Retail Prices resolver.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=public-ip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-ip.test.d.ts","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/public-ip.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Tests for the Public IP Retail Prices resolver.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { DiskCache } from "../../cache.js";
6
+ import { PricingClient } from "../../client.js";
7
+ import { makeTestCacheDir, removeTestCacheDir } from "../../test-cache-dir.js";
8
+ import { resolvePublicIpMonthlyPrice } from "../public-ip.js";
9
+ import { HOURS_PER_MONTH } from "../vm.js";
10
+ function makeFetch(body) {
11
+ return vi.fn().mockResolvedValue({
12
+ json: async () => body,
13
+ ok: true,
14
+ status: 200,
15
+ statusText: "OK",
16
+ });
17
+ }
18
+ let dir;
19
+ beforeEach(async () => {
20
+ dir = await makeTestCacheDir("public-ip-resolver");
21
+ });
22
+ afterEach(async () => {
23
+ await removeTestCacheDir(dir);
24
+ });
25
+ describe("resolvePublicIpMonthlyPrice", () => {
26
+ it("returns the monthly cost of a Standard static Public IP", async () => {
27
+ const body = {
28
+ Items: [
29
+ {
30
+ armRegionName: "westeurope",
31
+ currencyCode: "EUR",
32
+ meterName: "Standard Static IP Address",
33
+ productName: "Virtual Network",
34
+ retailPrice: 0.004,
35
+ skuName: "Standard",
36
+ type: "Consumption",
37
+ unitOfMeasure: "1 Hour",
38
+ unitPrice: 0.004,
39
+ },
40
+ ],
41
+ NextPageLink: null,
42
+ };
43
+ const client = new PricingClient({
44
+ cache: new DiskCache({ dir }),
45
+ fetch: makeFetch(body),
46
+ });
47
+ const money = await resolvePublicIpMonthlyPrice(client, {
48
+ allocation: "static",
49
+ armRegionName: "westeurope",
50
+ sku: "standard",
51
+ });
52
+ expect(money).toEqual({
53
+ amount: Math.round(0.004 * HOURS_PER_MONTH * 100) / 100,
54
+ currency: "EUR",
55
+ });
56
+ });
57
+ it("filters by allocation token in meterName", async () => {
58
+ const body = {
59
+ Items: [
60
+ {
61
+ armRegionName: "westeurope",
62
+ currencyCode: "EUR",
63
+ meterName: "Basic Dynamic IP Address",
64
+ productName: "Virtual Network",
65
+ retailPrice: 0.0036,
66
+ skuName: "Basic",
67
+ type: "Consumption",
68
+ unitOfMeasure: "1 Hour",
69
+ unitPrice: 0.0036,
70
+ },
71
+ {
72
+ armRegionName: "westeurope",
73
+ currencyCode: "EUR",
74
+ meterName: "Basic Static IP Address",
75
+ productName: "Virtual Network",
76
+ retailPrice: 0.0036,
77
+ skuName: "Basic",
78
+ type: "Consumption",
79
+ unitOfMeasure: "1 Hour",
80
+ unitPrice: 0.0036,
81
+ },
82
+ ],
83
+ NextPageLink: null,
84
+ };
85
+ const client = new PricingClient({
86
+ cache: new DiskCache({ dir }),
87
+ fetch: makeFetch(body),
88
+ });
89
+ const money = await resolvePublicIpMonthlyPrice(client, {
90
+ allocation: "dynamic",
91
+ armRegionName: "westeurope",
92
+ sku: "basic",
93
+ });
94
+ expect(money?.amount).toBeCloseTo(0.0036 * HOURS_PER_MONTH, 2);
95
+ });
96
+ it("returns undefined when no meter matches", async () => {
97
+ const body = {
98
+ Items: [],
99
+ NextPageLink: null,
100
+ };
101
+ const client = new PricingClient({
102
+ cache: new DiskCache({ dir }),
103
+ fetch: makeFetch(body),
104
+ });
105
+ const money = await resolvePublicIpMonthlyPrice(client, {
106
+ allocation: "static",
107
+ armRegionName: "westeurope",
108
+ sku: "standard",
109
+ });
110
+ expect(money).toBeUndefined();
111
+ });
112
+ });
113
+ //# sourceMappingURL=public-ip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-ip.test.js","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/public-ip.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tests for the VM Retail Prices resolver.
3
+ *
4
+ * The Retail Prices API is mocked via the `FetchLike` indirection so the
5
+ * tests run hermetically. Fixtures live next to the tests to keep the
6
+ * surface explicit.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=vm.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm.test.d.ts","sourceRoot":"","sources":["../../../../../src/azure/pricing/resolvers/__tests__/vm.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}