@jjlmoya/utils-finance 1.46.0 → 1.47.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 +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/loanAprScenarioCalculator/bibliography.astro +11 -0
- package/src/tool/loanAprScenarioCalculator/bibliography.ts +29 -0
- package/src/tool/loanAprScenarioCalculator/component.astro +62 -0
- package/src/tool/loanAprScenarioCalculator/controller.ts +205 -0
- package/src/tool/loanAprScenarioCalculator/currency-dom.ts +107 -0
- package/src/tool/loanAprScenarioCalculator/currency.test.ts +57 -0
- package/src/tool/loanAprScenarioCalculator/currency.ts +105 -0
- package/src/tool/loanAprScenarioCalculator/dom-views.ts +190 -0
- package/src/tool/loanAprScenarioCalculator/entry.ts +28 -0
- package/src/tool/loanAprScenarioCalculator/evaluator.ts +21 -0
- package/src/tool/loanAprScenarioCalculator/i18n/de.ts +194 -0
- package/src/tool/loanAprScenarioCalculator/i18n/en.ts +194 -0
- package/src/tool/loanAprScenarioCalculator/i18n/es.ts +195 -0
- package/src/tool/loanAprScenarioCalculator/i18n/fr.ts +195 -0
- package/src/tool/loanAprScenarioCalculator/i18n/id.ts +193 -0
- package/src/tool/loanAprScenarioCalculator/i18n/it.ts +193 -0
- package/src/tool/loanAprScenarioCalculator/i18n/ja.ts +186 -0
- package/src/tool/loanAprScenarioCalculator/i18n/ko.ts +186 -0
- package/src/tool/loanAprScenarioCalculator/i18n/nl.ts +194 -0
- package/src/tool/loanAprScenarioCalculator/i18n/pl.ts +194 -0
- package/src/tool/loanAprScenarioCalculator/i18n/pt.ts +194 -0
- package/src/tool/loanAprScenarioCalculator/i18n/ru.ts +193 -0
- package/src/tool/loanAprScenarioCalculator/i18n/sv.ts +189 -0
- package/src/tool/loanAprScenarioCalculator/i18n/tr.ts +193 -0
- package/src/tool/loanAprScenarioCalculator/i18n/zh.ts +181 -0
- package/src/tool/loanAprScenarioCalculator/index.ts +11 -0
- package/src/tool/loanAprScenarioCalculator/loan-apr-scenario-calculator.css +699 -0
- package/src/tool/loanAprScenarioCalculator/logic.test.ts +65 -0
- package/src/tool/loanAprScenarioCalculator/logic.ts +259 -0
- package/src/tool/loanAprScenarioCalculator/presets.ts +51 -0
- package/src/tool/loanAprScenarioCalculator/scenario-dom.ts +101 -0
- package/src/tool/loanAprScenarioCalculator/seo.astro +11 -0
- package/src/tool/loanAprScenarioCalculator/storage.ts +60 -0
- package/src/tool/loanAprScenarioCalculator/ui.ts +55 -0
- package/src/tool/loanAprScenarioCalculator/validation.ts +24 -0
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { LoanAprScenarioCalculatorUI } from "./ui";
|
|
2
|
+
import type { ComparisonResult, LoanScenarioResult } from "./logic";
|
|
3
|
+
import { currencyFormatLocale, type LoanAprCurrencyCode } from "./currency";
|
|
4
|
+
import { getDifferenceLabel, getScenarioBadge } from "./evaluator";
|
|
5
|
+
|
|
6
|
+
function money(
|
|
7
|
+
value: number,
|
|
8
|
+
locale: string,
|
|
9
|
+
currency: LoanAprCurrencyCode,
|
|
10
|
+
): string {
|
|
11
|
+
return new Intl.NumberFormat(currencyFormatLocale[currency] ?? locale, {
|
|
12
|
+
style: "currency",
|
|
13
|
+
currency,
|
|
14
|
+
maximumFractionDigits: 2,
|
|
15
|
+
}).format(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function percent(value: number): string {
|
|
19
|
+
return `${value.toFixed(2)}%`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeText(value: string): string {
|
|
23
|
+
return value.replace(
|
|
24
|
+
/[&<>"']/g,
|
|
25
|
+
(character) =>
|
|
26
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
27
|
+
character
|
|
28
|
+
] ?? character,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MetricTone = "better" | "worse" | "neutral" | "";
|
|
33
|
+
|
|
34
|
+
function metric(
|
|
35
|
+
label: string,
|
|
36
|
+
value: string,
|
|
37
|
+
emphasis = false,
|
|
38
|
+
tone: MetricTone = "",
|
|
39
|
+
): string {
|
|
40
|
+
const classes = [
|
|
41
|
+
"loan-apr-metric",
|
|
42
|
+
emphasis ? "loan-apr-metric-emphasis" : "",
|
|
43
|
+
tone ? `loan-apr-metric-${tone}` : "",
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ");
|
|
47
|
+
return `<div class="${classes}"><dt>${escapeText(label)}</dt><dd>${escapeText(value)}</dd></div>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function timeline(input: {
|
|
51
|
+
result: LoanScenarioResult;
|
|
52
|
+
side: "a" | "b";
|
|
53
|
+
ui: LoanAprScenarioCalculatorUI;
|
|
54
|
+
locale: string;
|
|
55
|
+
currency: LoanAprCurrencyCode;
|
|
56
|
+
}): string {
|
|
57
|
+
const { result, side, ui, locale, currency } = input;
|
|
58
|
+
const firstPayment = money(
|
|
59
|
+
result.monthlyPayment + result.monthlyFees,
|
|
60
|
+
locale,
|
|
61
|
+
currency,
|
|
62
|
+
);
|
|
63
|
+
const finalPayment = money(
|
|
64
|
+
result.monthlyPayment + result.monthlyFees + result.finalFees,
|
|
65
|
+
locale,
|
|
66
|
+
currency,
|
|
67
|
+
);
|
|
68
|
+
return `<div class="loan-apr-timeline loan-apr-timeline-${side}" aria-label="${escapeText(result.name)} ${escapeText(ui.labelTimeline)}"><div class="loan-apr-timeline-track"><span class="loan-apr-timeline-dot loan-apr-timeline-start"></span><span class="loan-apr-timeline-line"></span><span class="loan-apr-timeline-dot loan-apr-timeline-end"></span></div><div class="loan-apr-timeline-labels"><span>${escapeText(ui.labelStart)}<strong>${escapeText(money(result.netCashReceived, locale, currency))}</strong></span><span>${escapeText(ui.labelMonthly)}<strong>${escapeText(firstPayment)}</strong></span><span>${escapeText(ui.labelEnd)}<strong>${escapeText(finalPayment)}</strong></span></div></div>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function errorMessage(
|
|
72
|
+
error: string | undefined,
|
|
73
|
+
ui: LoanAprScenarioCalculatorUI,
|
|
74
|
+
): string {
|
|
75
|
+
if (error === "principal") return ui.labelInvalidPrincipal;
|
|
76
|
+
if (error === "term") return ui.labelInvalidTerm;
|
|
77
|
+
if (error === "fees") return ui.labelInvalidFees;
|
|
78
|
+
if (error === "rate") return ui.labelInvalidRate;
|
|
79
|
+
return ui.labelInvalidPayment;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ViewContext {
|
|
83
|
+
ui: LoanAprScenarioCalculatorUI;
|
|
84
|
+
locale: string;
|
|
85
|
+
currency: LoanAprCurrencyCode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function badgeText(
|
|
89
|
+
badge: ReturnType<typeof getScenarioBadge>,
|
|
90
|
+
ui: LoanAprScenarioCalculatorUI,
|
|
91
|
+
): string {
|
|
92
|
+
if (badge === "lower") return ui.labelLowerCost;
|
|
93
|
+
if (badge === "higher") return ui.labelHigherCost;
|
|
94
|
+
if (badge === "neutral") return ui.labelTie;
|
|
95
|
+
return ui.labelNeedValidInputs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function metricTone(
|
|
99
|
+
current: number,
|
|
100
|
+
other: number,
|
|
101
|
+
higherIsBetter = false,
|
|
102
|
+
): MetricTone {
|
|
103
|
+
if (!Number.isFinite(current) || !Number.isFinite(other)) return "";
|
|
104
|
+
if (Math.abs(current - other) < 0.005) return "neutral";
|
|
105
|
+
const currentIsBetter = higherIsBetter ? current > other : current < other;
|
|
106
|
+
return currentIsBetter ? "better" : "worse";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function visualComparison(
|
|
110
|
+
first: LoanScenarioResult,
|
|
111
|
+
second: LoanScenarioResult,
|
|
112
|
+
comparison: ComparisonResult,
|
|
113
|
+
ui: LoanAprScenarioCalculatorUI,
|
|
114
|
+
): string {
|
|
115
|
+
const firstBadge = getScenarioBadge("a", first, comparison);
|
|
116
|
+
const secondBadge = getScenarioBadge("b", second, comparison);
|
|
117
|
+
const aprValue = (result: LoanScenarioResult): string =>
|
|
118
|
+
result.valid ? percent(result.effectiveApr) : "-";
|
|
119
|
+
return `<div class="loan-apr-visual-comparison" aria-label="${escapeText(ui.labelEffectiveApr)}"><div class="loan-apr-visual-side loan-apr-visual-${firstBadge}"><span class="loan-apr-visual-index">A</span><span class="loan-apr-visual-name">${escapeText(first.name)}</span><strong>${escapeText(aprValue(first))}</strong><small>${escapeText(badgeText(firstBadge, ui))}</small></div><div class="loan-apr-visual-vs" aria-hidden="true">VS</div><div class="loan-apr-visual-side loan-apr-visual-${secondBadge}"><span class="loan-apr-visual-index">B</span><span class="loan-apr-visual-name">${escapeText(second.name)}</span><strong>${escapeText(aprValue(second))}</strong><small>${escapeText(badgeText(secondBadge, ui))}</small></div></div>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scenarioPanel(input: {
|
|
123
|
+
result: LoanScenarioResult;
|
|
124
|
+
other: LoanScenarioResult;
|
|
125
|
+
side: "a" | "b";
|
|
126
|
+
comparison: ComparisonResult;
|
|
127
|
+
context: ViewContext;
|
|
128
|
+
}): string {
|
|
129
|
+
const { result, other, side, comparison, context } = input;
|
|
130
|
+
const { ui, locale, currency } = context;
|
|
131
|
+
const badge = getScenarioBadge(side, result, comparison);
|
|
132
|
+
const statusText = badgeText(badge, ui);
|
|
133
|
+
if (!result.valid)
|
|
134
|
+
return `<article class="loan-apr-result-panel loan-apr-result-invalid"><header><span class="loan-apr-result-index">${side.toUpperCase()}</span><h3>${escapeText(result.name)}</h3><span class="loan-apr-badge loan-apr-badge-invalid">${escapeText(statusText)}</span></header><p>${escapeText(errorMessage(result.error, ui))}</p></article>`;
|
|
135
|
+
return `<article class="loan-apr-result-panel loan-apr-result-${badge}"><header><span class="loan-apr-result-index">${side.toUpperCase()}</span><h3>${escapeText(result.name)}</h3><span class="loan-apr-badge">${escapeText(statusText)}</span></header>${timeline({ result, side, ui, locale, currency })}<dl class="loan-apr-metrics">${metric(ui.labelEffectiveApr, percent(result.effectiveApr), true, metricTone(result.effectiveApr, other.effectiveApr))}${metric(ui.labelMonthlyPaymentResult, money(result.monthlyPayment, locale, currency), false, metricTone(result.monthlyPayment, other.monthlyPayment))}${metric(ui.labelTotalPaid, money(result.totalPaid, locale, currency), false, metricTone(result.totalPaid, other.totalPaid))}${metric(ui.labelTotalFees, money(result.totalFees, locale, currency), false, metricTone(result.totalFees, other.totalFees))}${metric(ui.labelFinanceCost, money(result.financeCost, locale, currency), false, metricTone(result.financeCost, other.financeCost))}${metric(ui.labelNetCashReceived, money(result.netCashReceived, locale, currency), false, metricTone(result.netCashReceived, other.netCashReceived, true))}</dl></article>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function winnerText(
|
|
139
|
+
first: LoanScenarioResult,
|
|
140
|
+
second: LoanScenarioResult,
|
|
141
|
+
comparison: ComparisonResult,
|
|
142
|
+
ui: LoanAprScenarioCalculatorUI,
|
|
143
|
+
): string {
|
|
144
|
+
if (comparison.winner === "a")
|
|
145
|
+
return `${first.name} ${ui.labelLowerCost.toLowerCase()}`;
|
|
146
|
+
if (comparison.winner === "b")
|
|
147
|
+
return `${second.name} ${ui.labelLowerCost.toLowerCase()}`;
|
|
148
|
+
if (comparison.winner === "tie") return ui.labelTie;
|
|
149
|
+
return ui.labelNoComparison;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderComparison(input: {
|
|
153
|
+
root: HTMLElement;
|
|
154
|
+
first: LoanScenarioResult;
|
|
155
|
+
second: LoanScenarioResult;
|
|
156
|
+
comparison: ComparisonResult;
|
|
157
|
+
context: ViewContext;
|
|
158
|
+
}): void {
|
|
159
|
+
const { root, first, second, comparison, context } = input;
|
|
160
|
+
const { ui } = context;
|
|
161
|
+
root.innerHTML = `<div class="loan-apr-result-intro"><div><h2>${escapeText(ui.labelWinner)}</h2></div><p>${escapeText(winnerText(first, second, comparison, ui))}<br><span>${escapeText(getDifferenceLabel(comparison))}</span></p></div>${visualComparison(first, second, comparison, ui)}<div class="loan-apr-result-panels">${scenarioPanel({ result: first, other: second, side: "a", comparison, context })}${scenarioPanel({ result: second, other: first, side: "b", comparison, context })}</div><div class="loan-apr-method"><span>${escapeText(ui.labelFormula)}</span><p>${escapeText(ui.labelFormulaText)}</p><small>${escapeText(ui.labelApproximation)}</small></div>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function copyWinner(
|
|
165
|
+
first: LoanScenarioResult,
|
|
166
|
+
second: LoanScenarioResult,
|
|
167
|
+
comparison: ComparisonResult,
|
|
168
|
+
ui: LoanAprScenarioCalculatorUI,
|
|
169
|
+
): string {
|
|
170
|
+
if (comparison.winner === "a") return first.name;
|
|
171
|
+
if (comparison.winner === "b") return second.name;
|
|
172
|
+
if (comparison.winner === "tie") return ui.labelTie;
|
|
173
|
+
return ui.labelNoComparison;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function copyLine(result: LoanScenarioResult, context: ViewContext): string {
|
|
177
|
+
if (!result.valid)
|
|
178
|
+
return `${result.name}: ${context.ui.labelNeedValidInputs}`;
|
|
179
|
+
return `${result.name}: ${percent(result.effectiveApr)} effective APR, ${money(result.monthlyPayment, context.locale, context.currency)} monthly payment, ${money(result.totalPaid, context.locale, context.currency)} total paid`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function formatResultForCopy(input: {
|
|
183
|
+
first: LoanScenarioResult;
|
|
184
|
+
second: LoanScenarioResult;
|
|
185
|
+
comparison: ComparisonResult;
|
|
186
|
+
context: ViewContext;
|
|
187
|
+
}): string {
|
|
188
|
+
const { first, second, comparison, context } = input;
|
|
189
|
+
return `${context.ui.labelWinner}: ${copyWinner(first, second, comparison, context.ui)}\n${copyLine(first, context)}\n${copyLine(second, context)}`;
|
|
190
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FinanceToolEntry, ToolLocaleContent } from "../../types";
|
|
2
|
+
import type { LoanAprScenarioCalculatorUI } from "./ui";
|
|
3
|
+
|
|
4
|
+
export type LoanAprScenarioCalculatorLocaleContent =
|
|
5
|
+
ToolLocaleContent<LoanAprScenarioCalculatorUI>;
|
|
6
|
+
|
|
7
|
+
export const loanAprScenarioCalculator: FinanceToolEntry<LoanAprScenarioCalculatorUI> =
|
|
8
|
+
{
|
|
9
|
+
id: "loan-apr-scenario-calculator",
|
|
10
|
+
icons: { bg: "mdi:cash-multiple", fg: "mdi:scale-balance" },
|
|
11
|
+
i18n: {
|
|
12
|
+
de: () => import("./i18n/de").then((module) => module.content),
|
|
13
|
+
en: () => import("./i18n/en").then((module) => module.content),
|
|
14
|
+
es: () => import("./i18n/es").then((module) => module.content),
|
|
15
|
+
fr: () => import("./i18n/fr").then((module) => module.content),
|
|
16
|
+
id: () => import("./i18n/id").then((module) => module.content),
|
|
17
|
+
it: () => import("./i18n/it").then((module) => module.content),
|
|
18
|
+
ja: () => import("./i18n/ja").then((module) => module.content),
|
|
19
|
+
ko: () => import("./i18n/ko").then((module) => module.content),
|
|
20
|
+
nl: () => import("./i18n/nl").then((module) => module.content),
|
|
21
|
+
pl: () => import("./i18n/pl").then((module) => module.content),
|
|
22
|
+
pt: () => import("./i18n/pt").then((module) => module.content),
|
|
23
|
+
ru: () => import("./i18n/ru").then((module) => module.content),
|
|
24
|
+
sv: () => import("./i18n/sv").then((module) => module.content),
|
|
25
|
+
tr: () => import("./i18n/tr").then((module) => module.content),
|
|
26
|
+
zh: () => import("./i18n/zh").then((module) => module.content),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComparisonResult, LoanScenarioResult } from "./logic";
|
|
2
|
+
|
|
3
|
+
export type ScenarioBadge = "lower" | "higher" | "neutral" | "invalid";
|
|
4
|
+
|
|
5
|
+
export function getScenarioBadge(
|
|
6
|
+
side: "a" | "b",
|
|
7
|
+
result: LoanScenarioResult,
|
|
8
|
+
comparison: ComparisonResult,
|
|
9
|
+
): ScenarioBadge {
|
|
10
|
+
if (!result.valid || comparison.message === "invalid") return "invalid";
|
|
11
|
+
if (comparison.winner === "tie") return "neutral";
|
|
12
|
+
return comparison.winner === side ? "lower" : "higher";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDifferenceLabel(comparison: ComparisonResult): string {
|
|
16
|
+
if (comparison.message === "invalid")
|
|
17
|
+
return "Enter valid values to compare both offers.";
|
|
18
|
+
if (comparison.message === "tie")
|
|
19
|
+
return "The two scenarios produce nearly the same effective annual cost.";
|
|
20
|
+
return `${comparison.difference.toFixed(2)} percentage points separate the effective annual costs.`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from "../../../types";
|
|
2
|
+
import { bibliography } from "../bibliography";
|
|
3
|
+
import type { LoanAprScenarioCalculatorUI } from "../ui";
|
|
4
|
+
|
|
5
|
+
const ui: LoanAprScenarioCalculatorUI = {
|
|
6
|
+
labelScenarioA: "Angebot A",
|
|
7
|
+
labelScenarioB: "Angebot B",
|
|
8
|
+
labelScenarioName: "Name des Angebots",
|
|
9
|
+
labelCurrency: "Währung",
|
|
10
|
+
labelApproximateRates:
|
|
11
|
+
"Näherungswerte für die Planung, keine Live-Wechselkurse.",
|
|
12
|
+
labelPrincipal: "Geliehener Betrag",
|
|
13
|
+
labelTerm: "Laufzeit",
|
|
14
|
+
labelMonths: "Monate",
|
|
15
|
+
labelInputBasis: "Welche Angabe kennen Sie?",
|
|
16
|
+
labelAnnualRate: "Nominaler Jahreszins",
|
|
17
|
+
labelMonthlyPayment: "Bekannte Monatsrate",
|
|
18
|
+
labelUpfrontFees: "Anfangsgebühren",
|
|
19
|
+
labelMonthlyFees: "Monatliche Gebühren",
|
|
20
|
+
labelFinalFees: "Schlussgebühr",
|
|
21
|
+
labelOfferNameA: "Angebot A",
|
|
22
|
+
labelOfferNameB: "Angebot B",
|
|
23
|
+
labelRateMode: "Ich kenne den Zinssatz",
|
|
24
|
+
labelPaymentMode: "Ich kenne die Monatsrate",
|
|
25
|
+
labelPreset: "Vergleich starten",
|
|
26
|
+
labelPresetBalanced: "Ausgewogene Angebote",
|
|
27
|
+
labelPresetLowFee: "Niedriger Zins mit Gebühren",
|
|
28
|
+
labelPresetLongTerm: "Längere Laufzeit",
|
|
29
|
+
labelReset: "Werte zurücksetzen",
|
|
30
|
+
labelCopy: "Vergleich kopieren",
|
|
31
|
+
labelCopied: "Vergleich kopiert",
|
|
32
|
+
labelEffectiveApr: "Effektive jährliche Kosten",
|
|
33
|
+
labelNominalRate: "Nominalzins",
|
|
34
|
+
labelMonthlyPaymentResult: "Monatsrate",
|
|
35
|
+
labelTotalPaid: "Gesamtzahlung",
|
|
36
|
+
labelTotalFees: "Gebühren im Szenario",
|
|
37
|
+
labelFinanceCost: "Kosten über dem Darlehen",
|
|
38
|
+
labelNetCashReceived: "Auszahlung nach Anfangsgebühren",
|
|
39
|
+
labelWinner: "Welches Angebot kostet weniger?",
|
|
40
|
+
labelLowerCost: "Geringere effektive Kosten",
|
|
41
|
+
labelHigherCost: "Höhere effektive Kosten",
|
|
42
|
+
labelTie: "Fast gleich",
|
|
43
|
+
labelNeedValidInputs: "Gültige Angaben erforderlich",
|
|
44
|
+
labelInvalidPrincipal:
|
|
45
|
+
"Der geliehene Betrag muss größer als null und höher als die Anfangsgebühren sein.",
|
|
46
|
+
labelInvalidTerm: "Verwenden Sie eine ganze Laufzeit von 1 bis 600 Monaten.",
|
|
47
|
+
labelInvalidFees: "Gebühren dürfen nicht negativ sein.",
|
|
48
|
+
labelInvalidPayment:
|
|
49
|
+
"Geben Sie eine positive Monatsrate ein, die die Auszahlung zurückzahlt.",
|
|
50
|
+
labelInvalidRate:
|
|
51
|
+
"Verwenden Sie einen nominalen Jahreszins von 0 % bis 100 %.",
|
|
52
|
+
labelNoComparison: "Vervollständigen Sie beide Angebote für den Vergleich.",
|
|
53
|
+
labelTimeline: "Zahlungsverlauf",
|
|
54
|
+
labelStart: "Start",
|
|
55
|
+
labelMonthly: "Monatlich",
|
|
56
|
+
labelEnd: "Letzter Monat",
|
|
57
|
+
labelFormula: "So funktioniert der Vergleich",
|
|
58
|
+
labelFormulaText:
|
|
59
|
+
"Die effektiven jährlichen Kosten sind der annualisierte Monatszins, bei dem Auszahlung und abgezinste Raten samt Gebühren übereinstimmen.",
|
|
60
|
+
labelApproximation:
|
|
61
|
+
"Die Schätzung setzt gleiche Monatsraten, keine tilgungsfreie Zeit und nur die eingegebenen Gebühren voraus. Sie ersetzt nicht die Angaben des Kreditgebers.",
|
|
62
|
+
labelScenarioSummary: "Szenarioübersicht",
|
|
63
|
+
currencyCode: "EUR",
|
|
64
|
+
currencyLocale: "de-DE",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const faq = [
|
|
68
|
+
{
|
|
69
|
+
question:
|
|
70
|
+
"Was ist der Unterschied zwischen Zinssatz und effektiven jährlichen Kosten?",
|
|
71
|
+
answer:
|
|
72
|
+
"Der nominale Zinssatz beschreibt die Zinsen auf den Saldo. Die effektiven jährlichen Kosten berücksichtigen zusätzlich Zeitpunkt und Höhe der eingegebenen Anfangs-, Monats- und Schlussgebühren.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: "Kann ich statt eines Zinssatzes eine Monatsrate eingeben?",
|
|
76
|
+
answer:
|
|
77
|
+
"Ja. Wählen Sie für das Angebot die bekannte Monatsrate. Der Rechner ermittelt den Monatszins, der Auszahlung, Zahlungsstrom und Gebühren dieses Szenarios entspricht.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
question: "Welches Angebot ist günstiger?",
|
|
81
|
+
answer:
|
|
82
|
+
"Vergleichen Sie zuerst die effektiven jährlichen Kosten und prüfen Sie danach Gesamtzahlung und Auszahlung. Bei unterschiedlicher Laufzeit oder Kredithöhe sollte die Entscheidung alle drei Signale berücksichtigen.",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
question: "Sind das die offiziellen Kreditkosten des Anbieters?",
|
|
86
|
+
answer:
|
|
87
|
+
"Nein. Es ist eine transparente Szenarioschätzung anhand Ihrer Eingaben. Kreditgeber und Länder können Kosten, Zahlungstermine, Steuern und Versicherungen anders berücksichtigen.",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const howTo = [
|
|
92
|
+
{
|
|
93
|
+
name: "Betrag und Laufzeit eingeben",
|
|
94
|
+
text: "Geben Sie für jedes Angebot den ausgezahlten Betrag und die Zahl der Monatsraten ein.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "Bekannte Angabe auswählen",
|
|
98
|
+
text: "Geben Sie je nach Angebot den nominalen Jahreszins oder die bekannte Monatsrate ein.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "Gebühren ergänzen",
|
|
102
|
+
text: "Tragen Sie Gebühren zu Beginn, pro Monat und am Ende ein, damit der Zahlungsstrom vollständig ist.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "Vergleich lesen",
|
|
106
|
+
text: "Nutzen Sie die effektiven jährlichen Kosten und prüfen Sie anschließend Gesamtzahlung und Auszahlung.",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const schemas = [
|
|
111
|
+
{
|
|
112
|
+
"@context": "https://schema.org",
|
|
113
|
+
"@type": "SoftwareApplication",
|
|
114
|
+
name: "Kredit APR Szenario Rechner",
|
|
115
|
+
description:
|
|
116
|
+
"Vergleicht zwei Kreditszenarien mit Zinsen, Raten und Gebühren.",
|
|
117
|
+
applicationCategory: "FinanceApplication",
|
|
118
|
+
operatingSystem: "All",
|
|
119
|
+
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"@context": "https://schema.org",
|
|
123
|
+
"@type": "FAQPage",
|
|
124
|
+
mainEntity: faq.map((item) => ({
|
|
125
|
+
"@type": "Question",
|
|
126
|
+
name: item.question,
|
|
127
|
+
acceptedAnswer: { "@type": "Answer", text: item.answer },
|
|
128
|
+
})),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"@context": "https://schema.org",
|
|
132
|
+
"@type": "HowTo",
|
|
133
|
+
name: "Zwei Kreditangebote vergleichen",
|
|
134
|
+
description: "Vergleicht Zahlungsströme und effektive jährliche Kosten.",
|
|
135
|
+
step: howTo.map((step, index) => ({
|
|
136
|
+
"@type": "HowToStep",
|
|
137
|
+
position: index + 1,
|
|
138
|
+
name: step.name,
|
|
139
|
+
text: step.text,
|
|
140
|
+
})),
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export const content: ToolLocaleContent<LoanAprScenarioCalculatorUI> = {
|
|
145
|
+
slug: "kredit-apr-szenario-rechner",
|
|
146
|
+
title: "Kredit APR Szenario Rechner",
|
|
147
|
+
description:
|
|
148
|
+
"Vergleichen Sie zwei Kreditangebote mit Zinsen, Zahlungsplan und Gebühren und schätzen Sie die effektiven jährlichen Kosten.",
|
|
149
|
+
ui,
|
|
150
|
+
faq,
|
|
151
|
+
bibliography,
|
|
152
|
+
howTo,
|
|
153
|
+
schemas,
|
|
154
|
+
seo: [
|
|
155
|
+
{
|
|
156
|
+
type: "title",
|
|
157
|
+
text: "Kreditangebote nach effektiven Kosten vergleichen",
|
|
158
|
+
level: 2,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "paragraph",
|
|
162
|
+
html: "<p>Ein niedriger Werbezins kann eine Bearbeitungsgebühr, eine monatliche Servicegebühr oder eine Schlusszahlung verbergen. Dieser Rechner macht daraus zwei vergleichbare Zahlungsströme und zeigt die Kosten des tatsächlich ausgezahlten Geldes.</p>",
|
|
163
|
+
},
|
|
164
|
+
{ type: "title", text: "Was die Schätzung enthält", level: 3 },
|
|
165
|
+
{
|
|
166
|
+
type: "paragraph",
|
|
167
|
+
html: "<p>Geben Sie Betrag, Laufzeit, Zinssatz oder Monatsrate sowie Gebühren zu verschiedenen Zeitpunkten ein. Der Rechner findet den Monatszins, der Auszahlung und abgezinste Zahlungen samt Gebühren ausgleicht, und annualisiert ihn.</p>",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
type: "paragraph",
|
|
171
|
+
html: "<p>Damit wird der Preis eines Kredits breiter betrachtet als nur der Nominalzins. Das Ergebnis bleibt eine Schätzung, weil offizielle Angaben je nach Land weitere Kosten und Annahmen enthalten können.</p>",
|
|
172
|
+
},
|
|
173
|
+
{ type: "title", text: "Ergebnis richtig lesen", level: 3 },
|
|
174
|
+
{
|
|
175
|
+
type: "list",
|
|
176
|
+
items: [
|
|
177
|
+
"Vergleichen Sie die effektiven Kosten bei gleicher Kredithöhe und Laufzeit.",
|
|
178
|
+
"Prüfen Sie die Gesamtzahlung für die absolute Belastung.",
|
|
179
|
+
"Beachten Sie die Auszahlung nach Anfangsgebühren.",
|
|
180
|
+
"Nutzen Sie den Zahlungsverlauf, um den Zeitpunkt jeder Kostenart zu sehen.",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "tip",
|
|
185
|
+
title: "Kreditunterlagen bleiben die Referenz",
|
|
186
|
+
html: "<p>Vergleichen Sie bei einem echten Vertrag die offiziellen Angaben des Kreditgebers mit denselben Annahmen. Fragen Sie nach Versicherungen, Steuern, variablen Zinsen und vorzeitigen Rückzahlungskosten.</p>",
|
|
187
|
+
},
|
|
188
|
+
{ type: "title", text: "Grenzen der Szenarioschätzung", level: 3 },
|
|
189
|
+
{
|
|
190
|
+
type: "paragraph",
|
|
191
|
+
html: "<p>Das Tool setzt gleiche Monatsraten, keine tilgungsfreie Zeit und Gebühren zu Beginn, während der Laufzeit oder im letzten Monat voraus. Es bestätigt weder Leistbarkeit noch Genehmigung oder den offiziellen APR eines Produkts.</p>",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from "../../../types";
|
|
2
|
+
import { bibliography } from "../bibliography";
|
|
3
|
+
import type { LoanAprScenarioCalculatorUI } from "../ui";
|
|
4
|
+
|
|
5
|
+
const ui: LoanAprScenarioCalculatorUI = {
|
|
6
|
+
labelScenarioA: "Offer A",
|
|
7
|
+
labelScenarioB: "Offer B",
|
|
8
|
+
labelScenarioName: "Offer name",
|
|
9
|
+
labelCurrency: "Currency",
|
|
10
|
+
labelApproximateRates:
|
|
11
|
+
"Uses approximate planning factors, not live exchange rates.",
|
|
12
|
+
labelPrincipal: "Amount borrowed",
|
|
13
|
+
labelTerm: "Repayment term",
|
|
14
|
+
labelMonths: "months",
|
|
15
|
+
labelInputBasis: "What do you know?",
|
|
16
|
+
labelAnnualRate: "Nominal annual rate",
|
|
17
|
+
labelMonthlyPayment: "Known monthly payment",
|
|
18
|
+
labelUpfrontFees: "Upfront fees",
|
|
19
|
+
labelMonthlyFees: "Monthly fees",
|
|
20
|
+
labelFinalFees: "Final fee",
|
|
21
|
+
labelOfferNameA: "Offer A",
|
|
22
|
+
labelOfferNameB: "Offer B",
|
|
23
|
+
labelRateMode: "I know the rate",
|
|
24
|
+
labelPaymentMode: "I know the payment",
|
|
25
|
+
labelPreset: "Start with a comparison",
|
|
26
|
+
labelPresetBalanced: "Balanced offers",
|
|
27
|
+
labelPresetLowFee: "Low rate with fees",
|
|
28
|
+
labelPresetLongTerm: "Longer term",
|
|
29
|
+
labelReset: "Reset values",
|
|
30
|
+
labelCopy: "Copy comparison",
|
|
31
|
+
labelCopied: "Comparison copied",
|
|
32
|
+
labelEffectiveApr: "Effective annual cost",
|
|
33
|
+
labelNominalRate: "Nominal rate",
|
|
34
|
+
labelMonthlyPaymentResult: "Monthly payment",
|
|
35
|
+
labelTotalPaid: "Total paid",
|
|
36
|
+
labelTotalFees: "Fees in the scenario",
|
|
37
|
+
labelFinanceCost: "Cost above principal",
|
|
38
|
+
labelNetCashReceived: "Cash received after upfront fees",
|
|
39
|
+
labelWinner: "Which offer costs less?",
|
|
40
|
+
labelLowerCost: "Lower effective cost",
|
|
41
|
+
labelHigherCost: "Higher effective cost",
|
|
42
|
+
labelTie: "Nearly tied",
|
|
43
|
+
labelNeedValidInputs: "Needs valid inputs",
|
|
44
|
+
labelInvalidPrincipal:
|
|
45
|
+
"The amount borrowed must be greater than the upfront fees and above zero.",
|
|
46
|
+
labelInvalidTerm: "Use a whole repayment term from 1 to 600 months.",
|
|
47
|
+
labelInvalidFees: "Fees cannot be negative.",
|
|
48
|
+
labelInvalidPayment:
|
|
49
|
+
"Enter a positive monthly payment that can repay the net cash received.",
|
|
50
|
+
labelInvalidRate: "Use a nominal annual rate from 0% to 100%.",
|
|
51
|
+
labelNoComparison: "Complete both offers to compare them.",
|
|
52
|
+
labelTimeline: "Payment timeline",
|
|
53
|
+
labelStart: "Start",
|
|
54
|
+
labelMonthly: "Each month",
|
|
55
|
+
labelEnd: "Final month",
|
|
56
|
+
labelFormula: "How the comparison works",
|
|
57
|
+
labelFormulaText:
|
|
58
|
+
"The effective annual cost is the annualised monthly rate that makes the cash received equal to the discounted payments and fees.",
|
|
59
|
+
labelApproximation:
|
|
60
|
+
"Estimate assumes equal monthly payments, no grace period and only the fees you enter. It does not replace the APR or TAE in a lender disclosure.",
|
|
61
|
+
labelScenarioSummary: "Scenario summary",
|
|
62
|
+
currencyCode: "USD",
|
|
63
|
+
currencyLocale: "en-US",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const faq = [
|
|
67
|
+
{
|
|
68
|
+
question:
|
|
69
|
+
"What is the difference between the interest rate and the effective annual cost?",
|
|
70
|
+
answer:
|
|
71
|
+
"The nominal interest rate describes the interest charged on the balance. The effective annual cost in this calculator also reflects the timing and amount of the upfront, monthly and final fees you enter.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
question: "Can I enter a monthly payment instead of an interest rate?",
|
|
75
|
+
answer:
|
|
76
|
+
"Yes. Choose the known monthly payment option for that offer. The calculator infers the monthly rate that matches the amount received, the payment stream and the fees in the scenario.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
question: "Which offer is cheaper?",
|
|
80
|
+
answer:
|
|
81
|
+
"Compare the effective annual cost first, then review total paid and cash received. A lower percentage can still have a higher total cost when the term or amount differs, so the decision should use all three signals.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
question: "Is this the official APR or TAE from a lender?",
|
|
85
|
+
answer:
|
|
86
|
+
"No. It is a transparent scenario estimate based on your inputs. Lenders and jurisdictions may include or exclude costs differently and may use different payment dates, taxes, insurance or variable-rate assumptions.",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const howTo = [
|
|
91
|
+
{
|
|
92
|
+
name: "Enter the amount and term",
|
|
93
|
+
text: "Add the amount you would receive and the number of monthly repayments for each offer.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Choose the known offer detail",
|
|
97
|
+
text: "Enter either the nominal annual rate or the monthly payment, depending on what the offer gives you.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "Add every relevant fee",
|
|
101
|
+
text: "Enter fees paid at the start, each month or at the end so the comparison reflects the cash flow you want to test.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "Read the cost scanner",
|
|
105
|
+
text: "Use effective annual cost to compare pricing, then check total paid and net cash received before deciding which offer fits your question.",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const schemas = [
|
|
110
|
+
{
|
|
111
|
+
"@context": "https://schema.org",
|
|
112
|
+
"@type": "SoftwareApplication",
|
|
113
|
+
name: "Loan APR scenario calculator",
|
|
114
|
+
description:
|
|
115
|
+
"Compare two loan scenarios with rates, payments and fees using a transparent effective annual cost estimate.",
|
|
116
|
+
applicationCategory: "FinanceApplication",
|
|
117
|
+
operatingSystem: "All",
|
|
118
|
+
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"@context": "https://schema.org",
|
|
122
|
+
"@type": "FAQPage",
|
|
123
|
+
mainEntity: faq.map((item) => ({
|
|
124
|
+
"@type": "Question",
|
|
125
|
+
name: item.question,
|
|
126
|
+
acceptedAnswer: { "@type": "Answer", text: item.answer },
|
|
127
|
+
})),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"@context": "https://schema.org",
|
|
131
|
+
"@type": "HowTo",
|
|
132
|
+
name: "Compare two loan offers with rates and fees",
|
|
133
|
+
description:
|
|
134
|
+
"Use the calculator to model two loan cash flows and compare their effective annual cost.",
|
|
135
|
+
step: howTo.map((step, index) => ({
|
|
136
|
+
"@type": "HowToStep",
|
|
137
|
+
position: index + 1,
|
|
138
|
+
name: step.name,
|
|
139
|
+
text: step.text,
|
|
140
|
+
})),
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export const content: ToolLocaleContent<LoanAprScenarioCalculatorUI> = {
|
|
145
|
+
slug: "loan-apr-scenario-calculator",
|
|
146
|
+
title: "Loan APR scenario calculator",
|
|
147
|
+
description:
|
|
148
|
+
"Compare two loan offers with their interest, payment schedule and fees to estimate the effective annual cost before you decide.",
|
|
149
|
+
ui,
|
|
150
|
+
faq,
|
|
151
|
+
bibliography,
|
|
152
|
+
howTo,
|
|
153
|
+
schemas,
|
|
154
|
+
seo: [
|
|
155
|
+
{
|
|
156
|
+
type: "title",
|
|
157
|
+
text: "Compare loan offers by effective annual cost",
|
|
158
|
+
level: 2,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "paragraph",
|
|
162
|
+
html: "<p>A low advertised interest rate can hide an arrangement fee, a monthly service charge or a final cost. This loan APR scenario calculator turns the details you have into two comparable cash flow scenarios, so you can see the cost of the money you actually receive.</p>",
|
|
163
|
+
},
|
|
164
|
+
{ type: "title", text: "What the estimate includes", level: 3 },
|
|
165
|
+
{
|
|
166
|
+
type: "paragraph",
|
|
167
|
+
html: "<p>For each offer, enter the amount borrowed, the repayment term, either the nominal annual rate or the known monthly payment, and the fees charged at different points in the loan. The calculator finds the monthly rate that balances the net cash received against the discounted payments and fees, then annualises it.</p>",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
type: "paragraph",
|
|
171
|
+
html: "<p>This mirrors the useful comparison principle behind APR and TAE: the price of credit is broader than the interest rate alone. The result is deliberately labelled an estimate because official disclosures may follow jurisdiction-specific rules and include assumptions that are not present in your scenario.</p>",
|
|
172
|
+
},
|
|
173
|
+
{ type: "title", text: "How to read the result", level: 3 },
|
|
174
|
+
{
|
|
175
|
+
type: "list",
|
|
176
|
+
items: [
|
|
177
|
+
"Compare effective annual cost when the amount and term are alike.",
|
|
178
|
+
"Check total paid to understand the absolute cash impact.",
|
|
179
|
+
"Check cash received after upfront fees so a fee is not mistaken for borrowed money.",
|
|
180
|
+
"Use the timeline to see when each cost enters the scenario.",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "tip",
|
|
185
|
+
title: "Use the lender disclosure as the final reference",
|
|
186
|
+
html: "<p>If you are comparing a real contract, match the lender's APR or TAE against the same assumptions. Ask about insurance, taxes, variable rates, early repayment charges and any fee that is not represented by your inputs.</p>",
|
|
187
|
+
},
|
|
188
|
+
{ type: "title", text: "Limits of a scenario estimate", level: 3 },
|
|
189
|
+
{
|
|
190
|
+
type: "paragraph",
|
|
191
|
+
html: "<p>This tool assumes equal monthly payments, no grace period and fees that you can place at the start, during the term or in the final month. It does not certify affordability, approval, legality or the official APR of a product. Use it to make a better comparison from the information you have, then verify the offer documents.</p>",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|