@jjlmoya/utils-finance 1.35.0 → 1.37.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 (80) hide show
  1. package/package.json +17 -5
  2. package/scripts/postinstall.mjs +2 -4
  3. package/src/category/index.ts +2 -1
  4. package/src/components/ProductionBreadcrumb.astro +134 -0
  5. package/src/components/ProductionStructuredData.astro +46 -0
  6. package/src/components/ProductionWidget.astro +182 -0
  7. package/src/entries.ts +4 -1
  8. package/src/i18n/header-ui.ts +15 -0
  9. package/src/i18n/language-ui.ts +9 -0
  10. package/src/i18n/languages.ts +24 -0
  11. package/src/identity/brands.ts +5 -0
  12. package/src/layouts/ProductionCategoryPage.astro +184 -0
  13. package/src/layouts/ProductionPage.astro +218 -0
  14. package/src/layouts/ProductionUtilityPage.astro +283 -0
  15. package/src/mfe/assets.ts +39 -0
  16. package/src/mfe/category-ui.ts +34 -0
  17. package/src/mfe/manifest.ts +45 -0
  18. package/src/mfe/routes.ts +50 -0
  19. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  20. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  21. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  22. package/src/pages/index.astro +1 -2
  23. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  24. package/src/pages/utilidades/[slug].astro +90 -0
  25. package/src/pages/utilidades/categorias/[category].astro +38 -0
  26. package/src/tests/diacritics_density.test.ts +1 -1
  27. package/src/tests/inverted_punctuation.test.ts +1 -1
  28. package/src/tests/locale_completeness.test.ts +2 -2
  29. package/src/tests/mfe_cache_contract.test.ts +11 -0
  30. package/src/tests/mfe_manifest.test.ts +28 -0
  31. package/src/tests/registry_contract.test.ts +67 -0
  32. package/src/tests/script_density.test.ts +94 -94
  33. package/src/tests/tool_validation.test.ts +2 -2
  34. package/src/tests/translation_copy.test.ts +2 -2
  35. package/src/tool/compoundInterest/index.ts +1 -0
  36. package/src/tool/courtFeeCalculator/index.ts +1 -0
  37. package/src/tool/debtSnowball/index.ts +1 -0
  38. package/src/tool/fireCalculator/index.ts +1 -0
  39. package/src/tool/ibanBicSwiftConverter/index.ts +1 -0
  40. package/src/tool/inflation/index.ts +1 -0
  41. package/src/tool/inflationPurchasingPower/bibliography.astro +14 -0
  42. package/src/tool/inflationPurchasingPower/bibliography.ts +12 -0
  43. package/src/tool/inflationPurchasingPower/component.astro +79 -0
  44. package/src/tool/inflationPurchasingPower/controller.ts +82 -0
  45. package/src/tool/inflationPurchasingPower/dom-views.ts +99 -0
  46. package/src/tool/inflationPurchasingPower/entry.ts +31 -0
  47. package/src/tool/inflationPurchasingPower/evaluator.ts +18 -0
  48. package/src/tool/inflationPurchasingPower/i18n/de.ts +56 -0
  49. package/src/tool/inflationPurchasingPower/i18n/en.ts +131 -0
  50. package/src/tool/inflationPurchasingPower/i18n/es.ts +44 -0
  51. package/src/tool/inflationPurchasingPower/i18n/fr.ts +44 -0
  52. package/src/tool/inflationPurchasingPower/i18n/id.ts +44 -0
  53. package/src/tool/inflationPurchasingPower/i18n/it.ts +44 -0
  54. package/src/tool/inflationPurchasingPower/i18n/ja.ts +44 -0
  55. package/src/tool/inflationPurchasingPower/i18n/ko.ts +44 -0
  56. package/src/tool/inflationPurchasingPower/i18n/nl.ts +44 -0
  57. package/src/tool/inflationPurchasingPower/i18n/pl.ts +44 -0
  58. package/src/tool/inflationPurchasingPower/i18n/pt.ts +44 -0
  59. package/src/tool/inflationPurchasingPower/i18n/ru.ts +44 -0
  60. package/src/tool/inflationPurchasingPower/i18n/sv.ts +44 -0
  61. package/src/tool/inflationPurchasingPower/i18n/tr.ts +44 -0
  62. package/src/tool/inflationPurchasingPower/i18n/zh.ts +44 -0
  63. package/src/tool/inflationPurchasingPower/index.ts +11 -0
  64. package/src/tool/inflationPurchasingPower/inflation-purchasing-power-timeline.css +402 -0
  65. package/src/tool/inflationPurchasingPower/logic.test.ts +35 -0
  66. package/src/tool/inflationPurchasingPower/logic.ts +92 -0
  67. package/src/tool/inflationPurchasingPower/seo.astro +14 -0
  68. package/src/tool/inflationPurchasingPower/storage.ts +29 -0
  69. package/src/tool/inflationPurchasingPower/ui.ts +42 -0
  70. package/src/tool/lateInterest/index.ts +1 -0
  71. package/src/tool/legalInterestRate/index.ts +1 -0
  72. package/src/tool/lotteryOptimizer/index.ts +1 -0
  73. package/src/tool/mortgage/index.ts +1 -0
  74. package/src/tool/percentageCalculator/index.ts +1 -0
  75. package/src/tool/rentIncreaseCalculator/index.ts +1 -0
  76. package/src/tools.ts +2 -1
  77. package/src/types.ts +2 -4
  78. package/src/worker.ts +25 -0
  79. package/src/pages/[locale]/[slug].astro +0 -161
  80. package/src/pages/[locale].astro +0 -251
@@ -0,0 +1,402 @@
1
+ :root {
2
+ --n-ink: #25251f;
3
+ --n-muted: #706d62;
4
+ --n-paper: #fbf7ec;
5
+ --n-surface: #f1ead9;
6
+ --n-edge: #d5c9ae;
7
+ --n-accent: #b15c38;
8
+ --n-accent-soft: #f1d8c9;
9
+ --n-cool: #376b73;
10
+ --n-gold: #bd8a35;
11
+ --n-on-accent: #fffaf1;
12
+ --n-shadow: #51463122;
13
+ }
14
+
15
+ .theme-dark {
16
+ --n-ink: #f4efe4;
17
+ --n-muted: #bcb5a7;
18
+ --n-paper: #24251f;
19
+ --n-surface: #2e3028;
20
+ --n-edge: #555443;
21
+ --n-accent: #e28a61;
22
+ --n-accent-soft: #5a3429;
23
+ --n-cool: #86c3c0;
24
+ --n-gold: #e3b65f;
25
+ --n-on-accent: #271d19;
26
+ --n-shadow: #0006;
27
+ }
28
+
29
+ .inflation-purchasing-power-tool {
30
+ --n-card-ink: var(--n-ink);
31
+ --n-card-muted: var(--n-muted);
32
+ --n-card-paper: var(--n-paper);
33
+ --n-card-surface: var(--n-surface);
34
+ --n-card-edge: var(--n-edge);
35
+ --n-card-accent: var(--n-accent);
36
+ --n-card-cool: var(--n-cool);
37
+ --n-card-gold: var(--n-gold);
38
+
39
+ color: var(--n-card-ink);
40
+ background: var(--n-card-paper);
41
+ border: 1px solid var(--n-card-edge);
42
+ border-radius: 1.4rem;
43
+ box-shadow: 0 1rem 2.5rem var(--n-shadow);
44
+ overflow: hidden;
45
+ max-width: 66rem;
46
+ width: min(100%, calc(100vw - 2rem));
47
+ margin: 0 auto;
48
+ }
49
+
50
+ .ipp-controls {
51
+ display: grid;
52
+ grid-template-columns: repeat(12, 1fr);
53
+ gap: 1rem;
54
+ padding: 1.35rem;
55
+ background: var(--n-card-surface);
56
+ border-bottom: 1px solid var(--n-card-edge);
57
+ }
58
+
59
+ .ipp-field {
60
+ display: grid;
61
+ gap: 0.4rem;
62
+ grid-column: span 3;
63
+ min-width: 0;
64
+ }
65
+
66
+ .ipp-field label,
67
+ .ipp-flow legend {
68
+ color: var(--n-card-ink);
69
+ font-size: 0.8rem;
70
+ font-weight: 750;
71
+ }
72
+
73
+ .ipp-field small {
74
+ color: var(--n-card-muted);
75
+ font-size: 0.72rem;
76
+ line-height: 1.35;
77
+ }
78
+
79
+ .ipp-field input {
80
+ box-sizing: border-box;
81
+ width: 100%;
82
+ min-height: 3.1rem;
83
+ padding: 0.7rem 0.8rem;
84
+ color: var(--n-card-ink);
85
+ background: var(--n-card-paper);
86
+ border: 1px solid var(--n-card-edge);
87
+ border-radius: 0.55rem;
88
+ font: inherit;
89
+ font-size: 1.05rem;
90
+ font-variant-numeric: tabular-nums;
91
+ }
92
+
93
+ .ipp-field input:disabled {
94
+ opacity: 0.48;
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ .ipp-field input:focus,
99
+ .ipp-flow button:focus-visible,
100
+ .ipp-method summary:focus-visible {
101
+ outline: 3px solid var(--n-card-accent);
102
+ outline-offset: 2px;
103
+ }
104
+
105
+ .ipp-flow {
106
+ grid-column: span 12;
107
+ border: 0;
108
+ padding: 0;
109
+ margin: 0;
110
+ }
111
+
112
+ .ipp-flow-options {
113
+ display: flex;
114
+ gap: 0.5rem;
115
+ margin-top: 0.65rem;
116
+ }
117
+
118
+ .ipp-flow button {
119
+ min-height: 2.8rem;
120
+ padding: 0.55rem 0.85rem;
121
+ color: var(--n-card-muted);
122
+ background: var(--n-card-paper);
123
+ border: 1px solid var(--n-card-edge);
124
+ border-radius: 99rem;
125
+ font: inherit;
126
+ cursor: pointer;
127
+ }
128
+
129
+ .ipp-flow button.is-active {
130
+ color: var(--n-on-accent);
131
+ background: var(--n-card-accent);
132
+ border-color: var(--n-card-accent);
133
+ }
134
+
135
+ .ipp-output {
136
+ display: grid;
137
+ grid-template-columns: 1.2fr 0.8fr;
138
+ border-bottom: 1px solid var(--n-card-edge);
139
+ }
140
+
141
+ .ipp-scene-wrap {
142
+ padding: 1.25rem 1.1rem 1.1rem 1.35rem;
143
+ border-right: 1px solid var(--n-card-edge);
144
+ }
145
+
146
+ .ipp-scene-heading {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.6rem;
150
+ color: var(--n-card-muted);
151
+ font-size: 0.72rem;
152
+ letter-spacing: 0.12em;
153
+ text-transform: uppercase;
154
+ }
155
+
156
+ .ipp-scene-rule {
157
+ height: 1px;
158
+ flex: 1;
159
+ background: var(--n-card-edge);
160
+ }
161
+
162
+ .ipp-chart {
163
+ display: block;
164
+ width: 100%;
165
+ height: auto;
166
+ margin-top: 0.65rem;
167
+ }
168
+
169
+ .ipp-grid-line,
170
+ .ipp-axis {
171
+ stroke: var(--n-card-edge);
172
+ stroke-width: 1;
173
+ }
174
+
175
+ .ipp-grid-line {
176
+ stroke-dasharray: 4 8;
177
+ }
178
+
179
+ .ipp-line {
180
+ fill: none;
181
+ stroke-linecap: round;
182
+ stroke-linejoin: round;
183
+ stroke-width: 4;
184
+ }
185
+
186
+ .ipp-line-cost {
187
+ stroke: var(--n-card-gold);
188
+ }
189
+
190
+ .ipp-line-real {
191
+ stroke: var(--n-card-accent);
192
+ }
193
+
194
+ .ipp-line-balance {
195
+ stroke: var(--n-card-cool);
196
+ stroke-dasharray: 7 8;
197
+ stroke-width: 3;
198
+ }
199
+
200
+ .ipp-end-dot {
201
+ stroke: var(--n-card-paper);
202
+ stroke-width: 3;
203
+ }
204
+
205
+ .ipp-dot-cost {
206
+ fill: var(--n-card-gold);
207
+ }
208
+
209
+ .ipp-dot-real {
210
+ fill: var(--n-card-accent);
211
+ }
212
+
213
+ .ipp-axis-label,
214
+ .ipp-chart-caption {
215
+ fill: var(--n-card-muted);
216
+ font-size: 11px;
217
+ }
218
+
219
+ .ipp-chart-caption {
220
+ letter-spacing: 0.04em;
221
+ }
222
+
223
+ .ipp-legend {
224
+ display: flex;
225
+ flex-wrap: wrap;
226
+ gap: 0.75rem 1rem;
227
+ color: var(--n-card-muted);
228
+ font-size: 0.73rem;
229
+ }
230
+
231
+ .ipp-legend-item {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ gap: 0.35rem;
235
+ }
236
+
237
+ .ipp-legend-line {
238
+ width: 1.35rem;
239
+ height: 0.2rem;
240
+ display: inline-block;
241
+ border-radius: 99rem;
242
+ }
243
+
244
+ .ipp-legend-cost { background: var(--n-card-gold); }
245
+ .ipp-legend-real { background: var(--n-card-accent); }
246
+ .ipp-legend-balance { background: var(--n-card-cool); }
247
+
248
+ .ipp-result-wrap {
249
+ padding: 1.25rem;
250
+ }
251
+
252
+ .ipp-primary-metric,
253
+ .ipp-metric {
254
+ display: grid;
255
+ gap: 0.3rem;
256
+ }
257
+
258
+ .ipp-primary-metric {
259
+ padding-bottom: 1rem;
260
+ border-bottom: 1px solid var(--n-card-edge);
261
+ }
262
+
263
+ .ipp-primary-metric span,
264
+ .ipp-metric span {
265
+ color: var(--n-card-muted);
266
+ font-size: 0.72rem;
267
+ font-weight: 700;
268
+ }
269
+
270
+ .ipp-primary-metric strong {
271
+ color: var(--n-card-accent);
272
+ font-size: clamp(1.75rem, 4vw, 2.55rem);
273
+ line-height: 1.1;
274
+ font-variant-numeric: tabular-nums;
275
+ }
276
+
277
+ .ipp-primary-metric small {
278
+ color: var(--n-card-muted);
279
+ font-size: 0.7rem;
280
+ }
281
+
282
+ .ipp-metrics {
283
+ display: grid;
284
+ grid-template-columns: 1fr 1fr;
285
+ gap: 0.8rem;
286
+ padding: 1rem 0;
287
+ }
288
+
289
+ .ipp-metric strong {
290
+ color: var(--n-card-ink);
291
+ font-size: 1.1rem;
292
+ font-variant-numeric: tabular-nums;
293
+ }
294
+
295
+ .ipp-metric-accent strong { color: var(--n-card-accent); }
296
+
297
+ .ipp-scenario {
298
+ margin: 0;
299
+ padding: 0.75rem;
300
+ color: var(--n-card-muted);
301
+ background: var(--n-card-surface);
302
+ border-left: 3px solid var(--n-card-cool);
303
+ font-size: 0.78rem;
304
+ line-height: 1.45;
305
+ }
306
+
307
+ .ipp-scenario[data-ipp-tone="warning"] { border-left-color: var(--n-card-accent); }
308
+
309
+ .ipp-result-content h3 {
310
+ margin: 1.1rem 0 0.55rem;
311
+ color: var(--n-card-ink);
312
+ font-size: 0.82rem;
313
+ }
314
+
315
+ .ipp-table-wrap { overflow-x: auto; }
316
+
317
+ .ipp-table-wrap table {
318
+ width: 100%;
319
+ border-collapse: collapse;
320
+ color: var(--n-card-muted);
321
+ font-size: 0.7rem;
322
+ font-variant-numeric: tabular-nums;
323
+ }
324
+
325
+ .ipp-table-wrap th,
326
+ .ipp-table-wrap td {
327
+ padding: 0.42rem 0.3rem;
328
+ text-align: right;
329
+ white-space: nowrap;
330
+ border-bottom: 1px solid var(--n-card-edge);
331
+ }
332
+
333
+ .ipp-table-wrap th:first-child,
334
+ .ipp-table-wrap td:first-child { text-align: left; }
335
+
336
+ .ipp-table-wrap thead th {
337
+ color: var(--n-card-ink);
338
+ font-size: 0.65rem;
339
+ font-weight: 700;
340
+ }
341
+
342
+ .ipp-error {
343
+ grid-column: 1 / -1;
344
+ padding: 2rem 1.35rem;
345
+ color: var(--n-card-accent);
346
+ font-size: 0.85rem;
347
+ }
348
+
349
+ .ipp-method {
350
+ padding: 0.9rem 1.35rem 1.05rem;
351
+ background: var(--n-card-surface);
352
+ }
353
+
354
+ .ipp-method summary {
355
+ color: var(--n-card-ink);
356
+ cursor: pointer;
357
+ font-size: 0.82rem;
358
+ font-weight: 700;
359
+ }
360
+
361
+ .ipp-method p {
362
+ max-width: 52rem;
363
+ color: var(--n-card-muted);
364
+ font-size: 0.8rem;
365
+ line-height: 1.55;
366
+ }
367
+
368
+ @media (max-width: 48rem) {
369
+ .inflation-purchasing-power-tool {
370
+ width: calc(100% - 3.2rem);
371
+ margin-left: 0;
372
+ margin-right: 0;
373
+ }
374
+ .ipp-output {
375
+ grid-template-columns: 1fr;
376
+ }
377
+ .ipp-scene-wrap {
378
+ border-right: 0;
379
+ border-bottom: 1px solid var(--n-card-edge);
380
+ }
381
+ .ipp-result-wrap { padding-top: 1.05rem; }
382
+ }
383
+
384
+ @media (max-width: 37rem) {
385
+ .ipp-controls { grid-template-columns: 1fr 1fr; }
386
+ .ipp-field { grid-column: span 1; }
387
+ .ipp-flow { grid-column: span 2; }
388
+ .ipp-flow-options { flex-wrap: wrap; }
389
+ .ipp-flow button { flex: 1 1 8rem; }
390
+ }
391
+
392
+ @media (max-width: 25rem) {
393
+ .ipp-controls { grid-template-columns: 1fr; }
394
+ .ipp-field,
395
+ .ipp-flow { grid-column: span 1; }
396
+ .ipp-metrics { grid-template-columns: 1fr; }
397
+ .ipp-scene-wrap,
398
+ .ipp-result-wrap {
399
+ padding-left: 0.9rem;
400
+ padding-right: 0.9rem;
401
+ }
402
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateInflationTimeline, getMilestoneYears } from './logic';
3
+
4
+ describe('calculateInflationTimeline', () => {
5
+ it('compounds the cost equivalent and discounts unchanged cash', () => {
6
+ const result = calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 5, years: 10, annualCashFlow: 0, cashFlowMode: 'none' });
7
+ expect(result?.priceFactor).toBe(1.63);
8
+ expect(result?.points.at(-1)?.costEquivalent).toBe(1628.89);
9
+ expect(result?.points.at(-1)?.unchangedRealValue).toBe(613.91);
10
+ expect(result?.purchasingPowerLossPercent).toBe(38.61);
11
+ });
12
+
13
+ it('adds annual contributions to the nominal balance and discounts the result', () => {
14
+ const result = calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 2, years: 5, annualCashFlow: 200, cashFlowMode: 'contribution' });
15
+ expect(result?.points.at(-1)?.nominalBalance).toBe(2000);
16
+ expect(result?.points.at(-1)?.realBalance).toBe(1811.46);
17
+ });
18
+
19
+ it('stops a withdrawal scenario at zero and reports depletion', () => {
20
+ const result = calculateInflationTimeline({ startingAmount: 500, annualInflationRate: 2, years: 5, annualCashFlow: 200, cashFlowMode: 'withdrawal' });
21
+ expect(result?.points.at(-1)?.nominalBalance).toBe(0);
22
+ expect(result?.depletionYear).toBe(3);
23
+ });
24
+
25
+ it('deduplicates milestone years for short timelines', () => {
26
+ expect(getMilestoneYears(1)).toEqual([0, 1]);
27
+ expect(getMilestoneYears(4)).toEqual([0, 1, 2, 3, 4]);
28
+ });
29
+
30
+ it('rejects invalid rates, periods and amounts', () => {
31
+ expect(calculateInflationTimeline({ startingAmount: 0, annualInflationRate: 2, years: 5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
32
+ expect(calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: -100, years: 5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
33
+ expect(calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 2, years: 5.5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,92 @@
1
+ export type CashFlowMode = 'none' | 'contribution' | 'withdrawal';
2
+
3
+ export interface InflationPurchasingPowerInput {
4
+ startingAmount: number;
5
+ annualInflationRate: number;
6
+ years: number;
7
+ annualCashFlow: number;
8
+ cashFlowMode: CashFlowMode;
9
+ }
10
+
11
+ export interface InflationTimelinePoint {
12
+ year: number;
13
+ costEquivalent: number;
14
+ unchangedRealValue: number;
15
+ nominalBalance: number;
16
+ realBalance: number;
17
+ }
18
+
19
+ export interface InflationPurchasingPowerResult {
20
+ input: InflationPurchasingPowerInput;
21
+ priceFactor: number;
22
+ purchasingPowerLossPercent: number;
23
+ points: InflationTimelinePoint[];
24
+ milestones: InflationTimelinePoint[];
25
+ depletionYear: number | null;
26
+ }
27
+
28
+ const round = (value: number): number => Number(value.toFixed(2));
29
+
30
+ function hasFiniteValues(input: InflationPurchasingPowerInput): boolean {
31
+ return [input.startingAmount, input.annualInflationRate, input.years, input.annualCashFlow].every(Number.isFinite);
32
+ }
33
+
34
+ function hasValidRanges(input: InflationPurchasingPowerInput): boolean {
35
+ return input.startingAmount > 0
36
+ && input.annualInflationRate > -100
37
+ && input.annualInflationRate <= 100
38
+ && Number.isInteger(input.years)
39
+ && input.years >= 1
40
+ && input.years <= 80
41
+ && input.annualCashFlow >= 0;
42
+ }
43
+
44
+ function hasValidCashFlowMode(input: InflationPurchasingPowerInput): boolean {
45
+ return ['none', 'contribution', 'withdrawal'].includes(input.cashFlowMode);
46
+ }
47
+
48
+ function isValidInput(input: InflationPurchasingPowerInput): boolean {
49
+ return hasFiniteValues(input) && hasValidRanges(input) && hasValidCashFlowMode(input);
50
+ }
51
+
52
+ function getNominalBalance(input: InflationPurchasingPowerInput, year: number): number {
53
+ const flow = input.cashFlowMode === 'contribution' ? input.annualCashFlow : -input.annualCashFlow;
54
+ if (input.cashFlowMode === 'none') return input.startingAmount;
55
+ return Math.max(0, input.startingAmount + flow * year);
56
+ }
57
+
58
+ function createPoint(input: InflationPurchasingPowerInput, year: number): InflationTimelinePoint {
59
+ const priceFactor = (1 + input.annualInflationRate / 100) ** year;
60
+ const nominalBalance = getNominalBalance(input, year);
61
+ return {
62
+ year,
63
+ costEquivalent: round(input.startingAmount * priceFactor),
64
+ unchangedRealValue: round(input.startingAmount / priceFactor),
65
+ nominalBalance: round(nominalBalance),
66
+ realBalance: round(nominalBalance / priceFactor),
67
+ };
68
+ }
69
+
70
+ export function getMilestoneYears(years: number): number[] {
71
+ return [...new Set([0, Math.round(years * 0.25), Math.round(years * 0.5), Math.round(years * 0.75), years])];
72
+ }
73
+
74
+ export function calculateInflationTimeline(input: InflationPurchasingPowerInput): InflationPurchasingPowerResult | null {
75
+ if (!isValidInput(input)) return null;
76
+ const points = Array.from({ length: input.years + 1 }, (_, year) => createPoint(input, year));
77
+ const milestones = getMilestoneYears(input.years)
78
+ .map((year) => points[year])
79
+ .filter((point): point is InflationTimelinePoint => point !== undefined);
80
+ const depletionYear = input.cashFlowMode === 'withdrawal' && input.annualCashFlow > 0
81
+ ? points.find((point) => point.nominalBalance === 0 && point.year > 0)?.year ?? null
82
+ : null;
83
+ const priceFactor = (1 + input.annualInflationRate / 100) ** input.years;
84
+ return {
85
+ input,
86
+ priceFactor: round(priceFactor),
87
+ purchasingPowerLossPercent: round((1 - 1 / priceFactor) * 100),
88
+ points,
89
+ milestones,
90
+ depletionYear,
91
+ };
92
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { inflationPurchasingPower } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await inflationPurchasingPower.i18n[locale]?.();
12
+ ---
13
+
14
+ {content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,29 @@
1
+ import type { InflationPurchasingPowerInput } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-finance-inflation-purchasing-power';
4
+
5
+ function isStoredState(value: Partial<InflationPurchasingPowerInput>): boolean {
6
+ return typeof value.startingAmount === 'number'
7
+ && typeof value.annualInflationRate === 'number'
8
+ && typeof value.years === 'number'
9
+ && typeof value.annualCashFlow === 'number';
10
+ }
11
+
12
+ export function loadInflationPurchasingPowerState(fallback: InflationPurchasingPowerInput): InflationPurchasingPowerInput {
13
+ try {
14
+ const stored = window.localStorage.getItem(STORAGE_KEY);
15
+ if (!stored) return fallback;
16
+ const parsed = JSON.parse(stored) as Partial<InflationPurchasingPowerInput>;
17
+ if (!isStoredState(parsed)) return fallback;
18
+ const cashFlowMode = parsed.cashFlowMode === 'contribution' || parsed.cashFlowMode === 'withdrawal' ? parsed.cashFlowMode : 'none';
19
+ return { ...fallback, ...parsed, cashFlowMode };
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ export function saveInflationPurchasingPowerState(state: InflationPurchasingPowerInput): void {
26
+ try {
27
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
28
+ } catch {}
29
+ }
@@ -0,0 +1,42 @@
1
+ export interface InflationPurchasingPowerUI extends Record<string, string> {
2
+ amountLabel: string;
3
+ amountHelp: string;
4
+ inflationLabel: string;
5
+ inflationHelp: string;
6
+ yearsLabel: string;
7
+ yearsHelp: string;
8
+ cashFlowLabel: string;
9
+ cashFlowHelp: string;
10
+ cashFlowModeLabel: string;
11
+ noCashFlowOption: string;
12
+ contributionOption: string;
13
+ withdrawalOption: string;
14
+ costEquivalentLabel: string;
15
+ realValueLabel: string;
16
+ nominalBalanceLabel: string;
17
+ purchasingPowerLossLabel: string;
18
+ todayLabel: string;
19
+ endLabel: string;
20
+ costLineLabel: string;
21
+ realLineLabel: string;
22
+ balanceLineLabel: string;
23
+ chartDescription: string;
24
+ chartNote: string;
25
+ milestoneTitle: string;
26
+ yearHeader: string;
27
+ costHeader: string;
28
+ realHeader: string;
29
+ balanceHeader: string;
30
+ methodTitle: string;
31
+ methodText: string;
32
+ limitationText: string;
33
+ emptyMessage: string;
34
+ contributionSummary: string;
35
+ withdrawalSummary: string;
36
+ noCashFlowSummary: string;
37
+ depletionWarning: string;
38
+ scenarioNote: string;
39
+ currencyHint: string;
40
+ yearsSuffix: string;
41
+ amountSuffix: string;
42
+ }
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { lateInterest } from './entry';
2
3
  export * from './entry';
3
4
  export const LATE_INTEREST_TOOL: ToolDefinition = {
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { legalInterestRate } from './entry';
2
3
  export * from './entry';
3
4
  export const LEGAL_INTEREST_RATE_TOOL: ToolDefinition = {
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { lotteryOptimizer } from './entry';
2
3
  export * from './entry';
3
4
  export const LOTTERY_OPTIMIZER_TOOL: ToolDefinition = {
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { mortgage } from './entry';
2
3
  export * from './entry';
3
4
  export const MORTGAGE_TOOL: ToolDefinition = {
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { percentageCalculator } from './entry';
2
3
  export * from './entry';
3
4
  export const PERCENTAGE_CALCULATOR_TOOL: ToolDefinition = {
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { rentIncrease } from './entry';
2
3
  export * from './entry';
3
4
  export const RENT_INCREASE_TOOL: ToolDefinition = {
package/src/tools.ts CHANGED
@@ -19,5 +19,6 @@ import { PESETA_VALUE_TOOL } from './tool/pesetaValue';
19
19
  import { SALARY_EMPLOYER_COST_TOOL } from './tool/salaryEmployerCost';
20
20
  import { DIVIDEND_YIELD_TOOL } from './tool/dividendYield';
21
21
  import { MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL } from './tool/mortgageExtraPaymentPlanner';
22
+ import { INFLATION_PURCHASING_POWER_TOOL } from './tool/inflationPurchasingPower';
22
23
 
23
- export const ALL_TOOLS: ToolDefinition[] = [COMPOUND_INTEREST_TOOL, FINANCIAL_FREEDOM_TOOL, PROFITABILITY_TOOL, MORTGAGE_TOOL, INFLATION_TOOL, PERCENTAGE_CALCULATOR_TOOL, LATE_INTEREST_TOOL, IBAN_BIC_SWIFT_TOOL, RENT_INCREASE_TOOL, LOTTERY_OPTIMIZER_TOOL, COURT_FEE_CALCULATOR_TOOL, LEGAL_INTEREST_RATE_TOOL, FIRE_CALCULATOR_TOOL, DEBT_SNOWBALL_TOOL, REVOLVING_CARD_CALCULATOR_TOOL, PESETA_VALUE_TOOL, SALARY_EMPLOYER_COST_TOOL, DIVIDEND_YIELD_TOOL, MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL];
24
+ export const ALL_TOOLS: ToolDefinition[] = [COMPOUND_INTEREST_TOOL, FINANCIAL_FREEDOM_TOOL, PROFITABILITY_TOOL, MORTGAGE_TOOL, INFLATION_TOOL, PERCENTAGE_CALCULATOR_TOOL, LATE_INTEREST_TOOL, IBAN_BIC_SWIFT_TOOL, RENT_INCREASE_TOOL, LOTTERY_OPTIMIZER_TOOL, COURT_FEE_CALCULATOR_TOOL, LEGAL_INTEREST_RATE_TOOL, FIRE_CALCULATOR_TOOL, DEBT_SNOWBALL_TOOL, REVOLVING_CARD_CALCULATOR_TOOL, PESETA_VALUE_TOOL, SALARY_EMPLOYER_COST_TOOL, DIVIDEND_YIELD_TOOL, MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL, INFLATION_PURCHASING_POWER_TOOL];
package/src/types.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import type { SEOSection } from '@jjlmoya/utils-shared';
2
+ import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
2
3
  import type { WithContext, Thing } from 'schema-dts';
3
4
 
4
5
  export type { SEOSection };
5
6
 
6
- export type KnownLocale =
7
- | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
- | 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
- | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
7
+ export type KnownLocale = UtilityLocale;
10
8
 
11
9
  export interface FAQItem {
12
10
  question: string;