@jjlmoya/utils-finance 1.23.0 → 1.24.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.
- package/package.json +2 -2
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/revolvingCardCalculator/bibliography.astro +14 -0
- package/src/tool/revolvingCardCalculator/bibliography.ts +24 -0
- package/src/tool/revolvingCardCalculator/component.astro +315 -0
- package/src/tool/revolvingCardCalculator/components/AmortizationTable.astro +57 -0
- package/src/tool/revolvingCardCalculator/components/CalculatorInputs.astro +117 -0
- package/src/tool/revolvingCardCalculator/components/ChartDisplay.astro +35 -0
- package/src/tool/revolvingCardCalculator/components/UsuryChecker.astro +32 -0
- package/src/tool/revolvingCardCalculator/constants.ts +36 -0
- package/src/tool/revolvingCardCalculator/entry.ts +31 -0
- package/src/tool/revolvingCardCalculator/i18n/de.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/en.ts +322 -0
- package/src/tool/revolvingCardCalculator/i18n/es.ts +153 -0
- package/src/tool/revolvingCardCalculator/i18n/fr.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/id.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/it.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/ja.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/ko.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/nl.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/pl.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/pt.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/ru.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/sv.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/tr.ts +84 -0
- package/src/tool/revolvingCardCalculator/i18n/zh.ts +84 -0
- package/src/tool/revolvingCardCalculator/index.ts +10 -0
- package/src/tool/revolvingCardCalculator/logic.ts +237 -0
- package/src/tool/revolvingCardCalculator/revolving-card-calculator.css +564 -0
- package/src/tool/revolvingCardCalculator/seo.astro +15 -0
- package/src/tool/revolvingCardCalculator/types.ts +54 -0
- package/src/tool/revolvingCardCalculator/ui.ts +58 -0
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { RevolvingInputs, RevolvingMarket, AmortizationResult, AmortizationRow, UsuryAnalysis } from './types';
|
|
2
|
+
|
|
3
|
+
interface MarketDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
referenceRate: number;
|
|
6
|
+
thresholdRate: number;
|
|
7
|
+
safeSummary: string;
|
|
8
|
+
warningSummary: string;
|
|
9
|
+
usuriousSummary: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MARKET_DEFINITIONS: Record<RevolvingMarket, MarketDefinition> = {
|
|
13
|
+
es: {
|
|
14
|
+
name: 'Spain (Banco de España)',
|
|
15
|
+
referenceRate: 18.0,
|
|
16
|
+
thresholdRate: 24.0,
|
|
17
|
+
safeSummary: 'The APR is below the average revolving credit card rate in Spain (~18%).',
|
|
18
|
+
warningSummary: 'The APR exceeds the Spanish average revolving rate (~18%). High risk of debt capitalization.',
|
|
19
|
+
usuriousSummary: 'Potentially null by usury: Spanish Supreme Court (STS 258/2023) rules contracts usurious if APR exceeds the average by more than 6 percentage points (~24%).',
|
|
20
|
+
},
|
|
21
|
+
us: {
|
|
22
|
+
name: 'United States (CFPB)',
|
|
23
|
+
referenceRate: 21.5,
|
|
24
|
+
thresholdRate: 30.0,
|
|
25
|
+
safeSummary: 'The APR is below the US national credit card average (~21.5%).',
|
|
26
|
+
warningSummary: 'The APR is above the US national average (~21.5%). High compounding risk.',
|
|
27
|
+
usuriousSummary: 'APR above 30% is considered predatory by US consumer advocates. Federal law does not cap credit card rates, but state usury laws may apply.',
|
|
28
|
+
},
|
|
29
|
+
uk: {
|
|
30
|
+
name: 'United Kingdom (FCA)',
|
|
31
|
+
referenceRate: 22.0,
|
|
32
|
+
thresholdRate: 35.0,
|
|
33
|
+
safeSummary: 'The APR is below the UK average credit card rate (~22%).',
|
|
34
|
+
warningSummary: 'The APR is above the UK average credit card rate (~22%).',
|
|
35
|
+
usuriousSummary: 'An APR above 35% is extremely high in the UK market. The FCA requires mandatory affordability assessments at these levels.',
|
|
36
|
+
},
|
|
37
|
+
eu: {
|
|
38
|
+
name: 'European Union (ECB reference)',
|
|
39
|
+
referenceRate: 16.5,
|
|
40
|
+
thresholdRate: 24.0,
|
|
41
|
+
safeSummary: 'The APR is within the range typical for EU revolving credit products.',
|
|
42
|
+
warningSummary: 'The APR is above the EU average for revolving cards. Check national usury laws in your country.',
|
|
43
|
+
usuriousSummary: 'APR likely exceeds national usury caps in force across EU member states.',
|
|
44
|
+
},
|
|
45
|
+
jp: {
|
|
46
|
+
name: 'Japan (Financial Services Agency)',
|
|
47
|
+
referenceRate: 14.0,
|
|
48
|
+
thresholdRate: 20.0,
|
|
49
|
+
safeSummary: 'The APR is within the Japanese interest rate cap limit set by the Money Lending Business Act (20%).',
|
|
50
|
+
warningSummary: 'The APR is approaching the Japanese legal cap of 20% (Money Lending Business Act).',
|
|
51
|
+
usuriousSummary: 'APR exceeds 20%: this violates the Japanese Money Lending Business Act (貸金業法). The contract may be illegal and the excess interest recoverable.',
|
|
52
|
+
},
|
|
53
|
+
kr: {
|
|
54
|
+
name: 'South Korea (FSC)',
|
|
55
|
+
referenceRate: 15.0,
|
|
56
|
+
thresholdRate: 20.0,
|
|
57
|
+
safeSummary: 'The APR is below the South Korean average credit card revolving rate.',
|
|
58
|
+
warningSummary: 'The APR is above the South Korean average revolving rate (~15%).',
|
|
59
|
+
usuriousSummary: 'APR exceeds 20%: this may violate the Interest Limitation Act (이자제한법) in South Korea, which caps non-bank consumer loans.',
|
|
60
|
+
},
|
|
61
|
+
cn: {
|
|
62
|
+
name: "China (People's Bank of China)",
|
|
63
|
+
referenceRate: 18.0,
|
|
64
|
+
thresholdRate: 24.0,
|
|
65
|
+
safeSummary: 'The APR is within the range typical for Chinese credit card revolving products.',
|
|
66
|
+
warningSummary: 'The APR is above the typical Chinese credit card revolving rate (~18%).',
|
|
67
|
+
usuriousSummary: "APR above 24%: under China's Supreme People's Court guidelines, the 24% threshold triggers enforceability limits on interest claims.",
|
|
68
|
+
},
|
|
69
|
+
br: {
|
|
70
|
+
name: 'Brazil (Banco Central do Brasil)',
|
|
71
|
+
referenceRate: 120.0,
|
|
72
|
+
thresholdRate: 350.0,
|
|
73
|
+
safeSummary: 'The APR is below the extremely high Brazilian revolving credit average (~120% p.a.).',
|
|
74
|
+
warningSummary: 'The APR is approaching dangerous levels in the Brazilian revolving credit market.',
|
|
75
|
+
usuriousSummary: 'APR at extreme levels. New regulation (Law 14.905/2024) capped revolving rates at ~220%. You may have legal grounds to contest.',
|
|
76
|
+
},
|
|
77
|
+
mx: {
|
|
78
|
+
name: 'Mexico (Banco de México)',
|
|
79
|
+
referenceRate: 40.0,
|
|
80
|
+
thresholdRate: 75.0,
|
|
81
|
+
safeSummary: 'The APR is below the Mexican average revolving credit card rate (~40%).',
|
|
82
|
+
warningSummary: 'The APR is above the Mexican average revolving rate (~40%). High compounding risk.',
|
|
83
|
+
usuriousSummary: 'APR above 75% in Mexico. Banco de México imposes transparency rules, and usury claims may be possible under civil code.',
|
|
84
|
+
},
|
|
85
|
+
pl: {
|
|
86
|
+
name: 'Poland (KNF / Civil Code)',
|
|
87
|
+
referenceRate: 14.0,
|
|
88
|
+
thresholdRate: 22.0,
|
|
89
|
+
safeSummary: 'The APR is below the Polish average revolving credit rate.',
|
|
90
|
+
warningSummary: 'The APR is above the Polish average revolving rate. Review your agreement.',
|
|
91
|
+
usuriousSummary: 'APR may exceed Polish Civil Code usury limits. The non-interest costs cap and the maximum interest rate are strictly regulated.',
|
|
92
|
+
},
|
|
93
|
+
id: {
|
|
94
|
+
name: 'Indonesia (OJK)',
|
|
95
|
+
referenceRate: 24.0,
|
|
96
|
+
thresholdRate: 36.0,
|
|
97
|
+
safeSummary: 'The APR is within the range allowed by OJK (Otoritas Jasa Keuangan) for credit cards.',
|
|
98
|
+
warningSummary: 'The APR is above the Indonesian average credit card rate (~24%).',
|
|
99
|
+
usuriousSummary: 'APR above OJK recommended limits. Excessive rates may be contested under Indonesian consumer protection law.',
|
|
100
|
+
},
|
|
101
|
+
tr: {
|
|
102
|
+
name: 'Turkey (BRSA / BDDK)',
|
|
103
|
+
referenceRate: 40.0,
|
|
104
|
+
thresholdRate: 60.0,
|
|
105
|
+
safeSummary: 'The APR is within the BRSA-set credit card rate range for Turkey.',
|
|
106
|
+
warningSummary: 'The APR is above the Turkish average credit card rate. Turkey has mandatory BRSA rate ceilings.',
|
|
107
|
+
usuriousSummary: 'APR above BRSA limits. Turkish banking law sets maximum credit card interest rates; excess interest is not legally collectible.',
|
|
108
|
+
},
|
|
109
|
+
ru: {
|
|
110
|
+
name: 'Russia (Bank of Russia)',
|
|
111
|
+
referenceRate: 25.0,
|
|
112
|
+
thresholdRate: 35.0,
|
|
113
|
+
safeSummary: 'The APR is within the Russian average credit card revolving rate.',
|
|
114
|
+
warningSummary: 'The APR is above the Russian average revolving rate (~25%). Compounding risk is high.',
|
|
115
|
+
usuriousSummary: 'APR above 35% in Russia. Federal law No. 353-FZ on consumer credit establishes maximum rate limits as published quarterly by the Bank of Russia.',
|
|
116
|
+
},
|
|
117
|
+
se: {
|
|
118
|
+
name: 'Sweden (Finansinspektionen)',
|
|
119
|
+
referenceRate: 19.0,
|
|
120
|
+
thresholdRate: 30.0,
|
|
121
|
+
safeSummary: 'The APR is below the Swedish average revolving credit rate.',
|
|
122
|
+
warningSummary: 'The APR is above the Swedish average revolving rate (~19%).',
|
|
123
|
+
usuriousSummary: 'APR above 30% in Sweden. The Consumer Credit Act and Finansinspektionen rules restrict excessive rates. Legal challenge may be possible.',
|
|
124
|
+
},
|
|
125
|
+
au: {
|
|
126
|
+
name: 'Australia (ASIC / NCCP)',
|
|
127
|
+
referenceRate: 20.0,
|
|
128
|
+
thresholdRate: 30.0,
|
|
129
|
+
safeSummary: 'The APR is below the Australian average credit card revolving rate.',
|
|
130
|
+
warningSummary: 'The APR is above the Australian average (~20%). ASIC requires responsible lending obligations.',
|
|
131
|
+
usuriousSummary: 'APR above 30% in Australia. The National Consumer Credit Protection Act (NCCP) may apply.',
|
|
132
|
+
},
|
|
133
|
+
ca: {
|
|
134
|
+
name: 'Canada (FCAC)',
|
|
135
|
+
referenceRate: 20.0,
|
|
136
|
+
thresholdRate: 26.0,
|
|
137
|
+
safeSummary: 'The APR is below the typical Canadian credit card revolving rate.',
|
|
138
|
+
warningSummary: 'The APR is above the Canadian average (~20%). The FCAC mandates full disclosure of costs.',
|
|
139
|
+
usuriousSummary: 'APR above 26% in Canada. The Criminal Code of Canada sets a 60% annual effective rate ceiling.',
|
|
140
|
+
},
|
|
141
|
+
custom: {
|
|
142
|
+
name: 'Custom Threshold',
|
|
143
|
+
referenceRate: 15.0,
|
|
144
|
+
thresholdRate: 25.0,
|
|
145
|
+
safeSummary: 'The APR is below your custom reference rate.',
|
|
146
|
+
warningSummary: 'The APR is above your custom reference rate.',
|
|
147
|
+
usuriousSummary: 'The APR exceeds your custom usury threshold.',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function computePayment(inputs: RevolvingInputs, initialBalance: number): number {
|
|
152
|
+
let payment = inputs.paymentType === 'fixed'
|
|
153
|
+
? inputs.paymentValue
|
|
154
|
+
: initialBalance * (inputs.paymentValue / 100);
|
|
155
|
+
|
|
156
|
+
if (payment < inputs.minPaymentValue) {
|
|
157
|
+
payment = inputs.minPaymentValue;
|
|
158
|
+
}
|
|
159
|
+
return payment;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildRow(month: number, initialBalance: number, payment: number, interestPaid: number): AmortizationRow {
|
|
163
|
+
const principalPaid = payment - interestPaid;
|
|
164
|
+
const finalBalance = Math.max(0, initialBalance - principalPaid);
|
|
165
|
+
return { month, initialBalance, interestPaid, principalPaid, payment, finalBalance };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function runAmortizationLoop(inputs: RevolvingInputs): { schedule: AmortizationRow[]; totalInterest: number; totalPaid: number; isInfinite: boolean } {
|
|
169
|
+
const schedule: AmortizationRow[] = [];
|
|
170
|
+
let balance = inputs.balance;
|
|
171
|
+
const monthlyRate = (inputs.apr / 100) / 12;
|
|
172
|
+
let totalInterest = 0;
|
|
173
|
+
let totalPaid = 0;
|
|
174
|
+
let month = 0;
|
|
175
|
+
const maxMonths = 1200;
|
|
176
|
+
|
|
177
|
+
while (balance > 0.01 && month < maxMonths) {
|
|
178
|
+
month++;
|
|
179
|
+
const interestPaid = balance * monthlyRate;
|
|
180
|
+
let payment = computePayment(inputs, balance);
|
|
181
|
+
|
|
182
|
+
if (payment > balance + interestPaid) {
|
|
183
|
+
payment = balance + interestPaid;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (payment <= interestPaid + 0.01) {
|
|
187
|
+
return { schedule, totalInterest, totalPaid, isInfinite: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const row = buildRow(month, balance, payment, interestPaid);
|
|
191
|
+
balance = row.finalBalance;
|
|
192
|
+
totalInterest += interestPaid;
|
|
193
|
+
totalPaid += payment;
|
|
194
|
+
schedule.push(row);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const isInfinite = month >= maxMonths && balance > 0.01;
|
|
198
|
+
return { schedule, totalInterest, totalPaid, isInfinite };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function calculateAmortization(inputs: RevolvingInputs): AmortizationResult {
|
|
202
|
+
const { schedule, totalInterest, totalPaid, isInfinite } = runAmortizationLoop(inputs);
|
|
203
|
+
return {
|
|
204
|
+
schedule,
|
|
205
|
+
totalInterest,
|
|
206
|
+
totalPaid,
|
|
207
|
+
monthsToPay: schedule.length,
|
|
208
|
+
isInfinite,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveUsuryStatus(apr: number, referenceRate: number, thresholdRate: number): 'safe' | 'warning' | 'usurious' {
|
|
213
|
+
if (apr >= thresholdRate) return 'usurious';
|
|
214
|
+
if (apr >= referenceRate) return 'warning';
|
|
215
|
+
return 'safe';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function analyzeUsury(apr: number, market: RevolvingMarket, customThreshold?: number): UsuryAnalysis {
|
|
219
|
+
const def = MARKET_DEFINITIONS[market];
|
|
220
|
+
const thresholdRate = market === 'custom' ? (customThreshold ?? 25.0) : def.thresholdRate;
|
|
221
|
+
const referenceRate = def.referenceRate;
|
|
222
|
+
const status = resolveUsuryStatus(apr, referenceRate, thresholdRate);
|
|
223
|
+
|
|
224
|
+
const detailsMap: Record<typeof status, string> = {
|
|
225
|
+
safe: def.safeSummary,
|
|
226
|
+
warning: def.warningSummary,
|
|
227
|
+
usurious: def.usuriousSummary,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
status,
|
|
232
|
+
marketName: def.name,
|
|
233
|
+
referenceRate,
|
|
234
|
+
thresholdRate,
|
|
235
|
+
details: detailsMap[status],
|
|
236
|
+
};
|
|
237
|
+
}
|