@jjlmoya/utils-finance 1.40.0 → 1.42.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 +3 -2
- package/src/entries.ts +8 -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/rentalDepositReturn/bibliography.astro +11 -0
- package/src/tool/rentalDepositReturn/bibliography.ts +6 -0
- package/src/tool/rentalDepositReturn/component.astro +53 -0
- package/src/tool/rentalDepositReturn/controller.ts +156 -0
- package/src/tool/rentalDepositReturn/currency.ts +67 -0
- package/src/tool/rentalDepositReturn/dom-views.ts +31 -0
- package/src/tool/rentalDepositReturn/entry.ts +28 -0
- package/src/tool/rentalDepositReturn/i18n/content-factory.ts +36 -0
- package/src/tool/rentalDepositReturn/i18n/de.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/en.ts +116 -0
- package/src/tool/rentalDepositReturn/i18n/es.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/fr.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/id.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/it.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/ja.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/ko.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/nl.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/pl.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/pt.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/ru.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/sv.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/tr.ts +3 -0
- package/src/tool/rentalDepositReturn/i18n/translations.ts +34 -0
- package/src/tool/rentalDepositReturn/i18n/zh.ts +3 -0
- package/src/tool/rentalDepositReturn/index.ts +11 -0
- package/src/tool/rentalDepositReturn/logic.test.ts +53 -0
- package/src/tool/rentalDepositReturn/logic.ts +108 -0
- package/src/tool/rentalDepositReturn/rental-deposit-return-calculator.css +591 -0
- package/src/tool/rentalDepositReturn/seo.astro +11 -0
- package/src/tool/rentalDepositReturn/ui.ts +53 -0
- package/src/tool/savingsRate/bibliography.astro +11 -0
- package/src/tool/savingsRate/bibliography.ts +12 -0
- package/src/tool/savingsRate/component.astro +326 -0
- package/src/tool/savingsRate/content.ts +1877 -0
- package/src/tool/savingsRate/entry.ts +31 -0
- package/src/tool/savingsRate/i18n/de.ts +3 -0
- package/src/tool/savingsRate/i18n/en.ts +1 -0
- package/src/tool/savingsRate/i18n/es.ts +3 -0
- package/src/tool/savingsRate/i18n/fr.ts +3 -0
- package/src/tool/savingsRate/i18n/id.ts +3 -0
- package/src/tool/savingsRate/i18n/it.ts +3 -0
- package/src/tool/savingsRate/i18n/ja.ts +3 -0
- package/src/tool/savingsRate/i18n/ko.ts +3 -0
- package/src/tool/savingsRate/i18n/nl.ts +3 -0
- package/src/tool/savingsRate/i18n/pl.ts +3 -0
- package/src/tool/savingsRate/i18n/pt.ts +3 -0
- package/src/tool/savingsRate/i18n/ru.ts +3 -0
- package/src/tool/savingsRate/i18n/sv.ts +3 -0
- package/src/tool/savingsRate/i18n/tr.ts +3 -0
- package/src/tool/savingsRate/i18n/zh.ts +3 -0
- package/src/tool/savingsRate/index.ts +11 -0
- package/src/tool/savingsRate/logic.test.ts +87 -0
- package/src/tool/savingsRate/logic.ts +121 -0
- package/src/tool/savingsRate/savings-rate-calculator.css +575 -0
- package/src/tool/savingsRate/seo.astro +11 -0
- package/src/tool/savingsRate/ui.ts +48 -0
- package/src/tools.ts +3 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { RentalDepositReturnUI } from '../ui';
|
|
5
|
+
|
|
6
|
+
const ui: RentalDepositReturnUI = {
|
|
7
|
+
depositLabel: 'Deposit paid',
|
|
8
|
+
depositHelp: 'Use the amount actually paid as the deposit, in the same currency as every line below.',
|
|
9
|
+
deductionsTitle: 'Items kept from the deposit',
|
|
10
|
+
deductionsHelp: 'Enter the landlord or agent\'s own breakdown. Mark a line disputed when you want to test the amount without it.',
|
|
11
|
+
descriptionLabel: 'Description',
|
|
12
|
+
descriptionPlaceholder: 'e.g. Final water bill',
|
|
13
|
+
amountLabel: 'Amount',
|
|
14
|
+
statusLabel: 'Deduction status',
|
|
15
|
+
confirmedOption: 'Confirmed',
|
|
16
|
+
disputedOption: 'Disputed',
|
|
17
|
+
addButton: 'Add deduction',
|
|
18
|
+
resetButton: 'Restore example',
|
|
19
|
+
newDeductionLabel: 'New deduction',
|
|
20
|
+
removeButton: 'Remove this deduction',
|
|
21
|
+
currencyLabel: 'Currency',
|
|
22
|
+
currencyHelp: 'All figures stay in this currency. No exchange rates are used.',
|
|
23
|
+
summaryTitle: 'Confirmed vs disputed',
|
|
24
|
+
estimatedReturnLabel: 'Estimated return',
|
|
25
|
+
undisputedReturnLabel: 'If disputed items are removed',
|
|
26
|
+
totalDeductionsLabel: 'Total deductions',
|
|
27
|
+
deductionBreakdownTitle: 'The ledger behind the result',
|
|
28
|
+
confirmedLabel: 'Confirmed items',
|
|
29
|
+
disputedLabel: 'Disputed items',
|
|
30
|
+
returnFlowTitle: 'Deposit to return',
|
|
31
|
+
returnFlowText: 'A clear split between what is being kept and what could come back.',
|
|
32
|
+
depositNode: 'Deposit in',
|
|
33
|
+
deductionNode: 'Kept for deductions',
|
|
34
|
+
returnNode: 'Estimated back',
|
|
35
|
+
overDeductedTitle: 'The deductions exceed the deposit',
|
|
36
|
+
overDeductedText: 'The entered breakdown is above the deposit by',
|
|
37
|
+
scenariosTitle: 'One dispute at a time',
|
|
38
|
+
scenariosText: 'These scenarios show the return if one disputed line is removed while all other lines stay in place.',
|
|
39
|
+
scenarioColumn: 'Removed line',
|
|
40
|
+
scenarioReturnColumn: 'Return if removed',
|
|
41
|
+
scenarioIncreaseColumn: 'Increase in return',
|
|
42
|
+
noDisputed: 'Mark a line as disputed to see a comparison scenario.',
|
|
43
|
+
emptyMessage: 'Add a deposit and use a description with a zero or positive amount for every deduction.',
|
|
44
|
+
copyButton: 'Copy claim summary',
|
|
45
|
+
copiedButton: 'Summary copied',
|
|
46
|
+
privacyNote: 'Your figures stay in this browser. Nothing is sent to a server.',
|
|
47
|
+
methodTitle: 'How this estimate works',
|
|
48
|
+
methodText: 'The tool adds every deduction, subtracts that total from the deposit and floors the estimated return at zero. A disputed scenario removes one disputed line from the total so you can compare the effect of that line.',
|
|
49
|
+
limitationText: 'This is a calculation of your own figures, not a legal decision. Deposit rules, allowable deductions, evidence requirements and deadlines vary by jurisdiction and agreement.',
|
|
50
|
+
reportTitle: 'Rental deposit return summary',
|
|
51
|
+
reportDeposit: 'Deposit paid',
|
|
52
|
+
reportDeductions: 'Deductions entered',
|
|
53
|
+
reportConfirmed: 'Confirmed deductions',
|
|
54
|
+
reportDisputed: 'Disputed deductions',
|
|
55
|
+
reportEstimatedReturn: 'Estimated return',
|
|
56
|
+
reportPotentialReturn: 'Return if disputed items are removed',
|
|
57
|
+
reportNote: 'Check the agreement, invoices, condition record and local process before making a claim.',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const faq = [
|
|
61
|
+
{ question: 'What does the rental deposit return calculator estimate?', answer: 'It totals the deduction amounts you enter and subtracts them from the deposit. It also shows the amount that would remain if disputed lines were removed one at a time.' },
|
|
62
|
+
{ question: 'Does the calculator decide whether a deduction is legal?', answer: 'No. It only transforms your own figures. The rules, evidence and deadlines for a deposit depend on the tenancy agreement and the jurisdiction.' },
|
|
63
|
+
{ question: 'What should I enter as a disputed item?', answer: 'Use a line for a charge you want to question, such as a repair, cleaning fee, unpaid bill or rent balance. The scenario shows how much the return changes if that line is left out.' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const howTo = [
|
|
67
|
+
{ name: 'Enter the deposit', text: 'Type the deposit amount that was actually paid and choose its currency.' },
|
|
68
|
+
{ name: 'Add the breakdown', text: 'Enter each deduction from the statement or settlement, with its amount and a confirmed or disputed status.' },
|
|
69
|
+
{ name: 'Compare the returns', text: 'Read the estimated return, the return without disputed items and the one-line dispute scenarios.' },
|
|
70
|
+
{ name: 'Copy the summary', text: 'Copy the figures into your own message or notes, then check the agreement and supporting documents before claiming.' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
74
|
+
'@context': 'https://schema.org',
|
|
75
|
+
'@type': 'FAQPage',
|
|
76
|
+
mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const howToSchema: WithContext<HowTo> = {
|
|
80
|
+
'@context': 'https://schema.org',
|
|
81
|
+
'@type': 'HowTo',
|
|
82
|
+
name: 'Estimate a rental deposit return',
|
|
83
|
+
description: 'Compare a rental deposit with the deductions in your own settlement.',
|
|
84
|
+
step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
88
|
+
'@context': 'https://schema.org',
|
|
89
|
+
'@type': 'SoftwareApplication',
|
|
90
|
+
name: 'Rental Deposit Return Calculator',
|
|
91
|
+
description: 'Estimate a rental deposit return from your own deductions and compare disputed items.',
|
|
92
|
+
applicationCategory: 'FinanceApplication',
|
|
93
|
+
operatingSystem: 'All',
|
|
94
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
95
|
+
inLanguage: 'en',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const content: ToolLocaleContent<RentalDepositReturnUI> = {
|
|
99
|
+
slug: 'rental-deposit-return-calculator',
|
|
100
|
+
title: 'Rental Deposit Return Calculator',
|
|
101
|
+
description: 'Estimate how much of your rental deposit could come back after itemising confirmed and disputed deductions.',
|
|
102
|
+
ui,
|
|
103
|
+
faq,
|
|
104
|
+
bibliography,
|
|
105
|
+
howTo,
|
|
106
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
107
|
+
seo: [
|
|
108
|
+
{ type: 'title', text: 'Turn a deposit statement into a return estimate', level: 2 },
|
|
109
|
+
{ type: 'paragraph', html: '<p>When a tenancy ends, a settlement can mix the deposit, unpaid bills, cleaning, repairs and other charges in one opaque figure. This browser-only calculator lets you enter that statement and see the arithmetic behind the estimated return.</p>' },
|
|
110
|
+
{ type: 'title', text: 'Separate confirmed and disputed deductions', level: 3 },
|
|
111
|
+
{ type: 'paragraph', html: '<p>Mark each line as confirmed or disputed. The main estimate includes every line, while the comparison shows what would come back if a disputed line were removed. That turns a vague disagreement into a specific amount to check against invoices, the condition record and the tenancy agreement.</p>' },
|
|
112
|
+
{ type: 'list', items: ['Deposit paid and total deductions', 'Return estimate with the full breakdown', 'Return if disputed items are removed', 'A copyable summary for your own claim notes'] },
|
|
113
|
+
{ type: 'title', text: 'A calculation, not a legal ruling', level: 3 },
|
|
114
|
+
{ type: 'paragraph', html: '<p>Deposit protection and deduction rules differ across countries, regions and agreements. The tool does not decide whether a charge is lawful, reasonable or evidenced, and it does not apply a universal deadline or percentage.</p>' },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RentalDepositLocaleCopy } from './content-factory';
|
|
2
|
+
|
|
3
|
+
type Seed = { title: string; description: string; ui: Record<string, string>; faq: Array<{ question: string; answer: string }>; howTo: Array<{ name: string; text: string }>; seoTitle: string; seoText: string; seoLimit: string };
|
|
4
|
+
|
|
5
|
+
const makeCopy = (seed: Seed): RentalDepositLocaleCopy => ({
|
|
6
|
+
title: seed.title,
|
|
7
|
+
description: seed.description,
|
|
8
|
+
ui: seed.ui,
|
|
9
|
+
faq: seed.faq,
|
|
10
|
+
howTo: seed.howTo,
|
|
11
|
+
seo: [
|
|
12
|
+
{ type: 'title', text: seed.seoTitle, level: 2 },
|
|
13
|
+
{ type: 'paragraph', html: `<p>${seed.seoText}</p>` },
|
|
14
|
+
{ type: 'title', text: seed.seoLimit, level: 3 },
|
|
15
|
+
{ type: 'paragraph', html: `<p>${seed.description}</p>` },
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const localizedCopies: Record<string, RentalDepositLocaleCopy> = {
|
|
20
|
+
sv: makeCopy({ title: 'Kalkylator för återbetalning av hyresdeposition', description: 'Uppskatta återbetalningen av din hyresdeposition genom att skilja bekräftade och bestridda avdrag åt.', ui: { depositLabel: 'Betald deposition', deductionsTitle: 'Belopp som hålls från depositionen', descriptionLabel: 'Beskrivning', amountLabel: 'Belopp', statusLabel: 'Status för avdrag', confirmedOption: 'Bekräftat', disputedOption: 'Bestritt', addButton: 'Lägg till avdrag', resetButton: 'Återställ exempel', currencyLabel: 'Valuta', summaryTitle: 'Bekräftat eller bestridt', estimatedReturnLabel: 'Beräknad återbetalning', undisputedReturnLabel: 'Utan bestridda avdrag', totalDeductionsLabel: 'Totala avdrag', confirmedLabel: 'Bekräftade avdrag', disputedLabel: 'Bestridda avdrag', returnFlowTitle: 'Deposition att återbetala', returnNode: 'Beräknad återbetalning', copyButton: 'Kopiera sammanfattning', copiedButton: 'Sammanfattning kopierad', methodTitle: 'Så fungerar uppskattningen' }, faq: [{ question: 'Vad uppskattar kalkylatorn?', answer: 'Den drar av de angivna beloppen från depositionen och visar effekten av varje bestridet avdrag.' }, { question: 'Avgör kalkylatorn om ett avdrag är lagligt?', answer: 'Nej. Den räknar bara om dina egna belopp.' }, { question: 'Vad är ett bestridet avdrag?', answer: 'En kostnad som du vill kontrollera.' }], howTo: [{ name: 'Ange depositionen', text: 'Skriv in depositionen och välj valuta.' }, { name: 'Lägg till avdrag', text: 'Ange varje avdrag med belopp och status.' }, { name: 'Jämför återbetalningar', text: 'Jämför resultatet med varje scenario.' }, { name: 'Kopiera sammanfattningen', text: 'Kopiera beloppen och kontrollera underlagen.' }], seoTitle: 'Gör en depositionsredovisning till en tydlig återbetalningsuppskattning', seoText: 'Ange deposition och avdrag för att se återbetalningen och effekten av varje bestridande.', seoLimit: 'En beräkning, inte ett juridiskt beslut' }),
|
|
21
|
+
de: makeCopy({ title: 'Rechner für die Rückzahlung der Mietkaution', description: 'Schätze die Rückzahlung deiner Mietkaution anhand bestätigter und strittiger Abzüge.', ui: { depositLabel: 'Gezahlte Kaution', deductionsTitle: 'Von der Kaution einbehalten', descriptionLabel: 'Beschreibung', amountLabel: 'Betrag', statusLabel: 'Status des Abzugs', confirmedOption: 'Bestätigt', disputedOption: 'Strittig', addButton: 'Abzug hinzufügen', resetButton: 'Beispiel wiederherstellen', currencyLabel: 'Währung', summaryTitle: 'Bestätigt oder strittig', estimatedReturnLabel: 'Geschätzte Rückzahlung', undisputedReturnLabel: 'Ohne strittige Abzüge', totalDeductionsLabel: 'Abzüge insgesamt', confirmedLabel: 'Bestätigte Abzüge', disputedLabel: 'Strittige Abzüge', returnFlowTitle: 'Kaution zurück', returnNode: 'Geschätzt zurück', copyButton: 'Zusammenfassung kopieren', copiedButton: 'Zusammenfassung kopiert', methodTitle: 'So funktioniert die Schätzung' }, faq: [{ question: 'Was schätzt der Rechner?', answer: 'Er zieht die eingegebenen Abzüge von der Kaution ab und zeigt Szenarien ohne strittige Positionen.' }, { question: 'Ist das eine Rechtsentscheidung?', answer: 'Nein. Regeln und Fristen hängen vom Vertrag und vom Ort ab.' }, { question: 'Was ist eine strittige Position?', answer: 'Eine Forderung, die du prüfen oder zurückweisen möchtest.' }], howTo: [{ name: 'Kaution eingeben', text: 'Gib die gezahlte Kaution ein und wähle die Währung.' }, { name: 'Abzüge hinzufügen', text: 'Trage jede Position mit Betrag und Status ein.' }, { name: 'Rückzahlung vergleichen', text: 'Vergleiche das Ergebnis mit den strittigen Szenarien.' }, { name: 'Zusammenfassung kopieren', text: 'Kopiere die Zahlen und prüfe Vertrag und Belege.' }], seoTitle: 'Eine Kautionsabrechnung in eine Rückzahlungsschätzung verwandeln', seoText: 'Gib Kaution und Abzüge ein, um bestätigte und strittige Positionen zu vergleichen.', seoLimit: 'Eine Berechnung, keine Rechtsentscheidung' }),
|
|
22
|
+
es: makeCopy({ title: 'Calculadora de devolución de fianza de alquiler', description: 'Estima cuánto podría devolverse de tu fianza después de separar cargos confirmados y discutidos.', ui: { depositLabel: 'Fianza pagada', deductionsTitle: 'Importes retenidos de la fianza', descriptionLabel: 'Descripción', amountLabel: 'Importe', statusLabel: 'Estado del cargo', confirmedOption: 'Confirmado', disputedOption: 'Discutido', addButton: 'Añadir cargo', resetButton: 'Restaurar ejemplo', currencyLabel: 'Moneda', summaryTitle: 'Confirmado frente a discutido', estimatedReturnLabel: 'Devolución estimada', undisputedReturnLabel: 'Sin cargos discutidos', totalDeductionsLabel: 'Cargos totales', confirmedLabel: 'Cargos confirmados', disputedLabel: 'Cargos discutidos', returnFlowTitle: 'Fianza a devolver', returnNode: 'Devolución estimada', copyButton: 'Copiar resumen', copiedButton: 'Resumen copiado', methodTitle: 'Cómo funciona esta estimación' }, faq: [{ question: '¿Qué estima esta calculadora?', answer: 'Resta de la fianza los cargos introducidos y muestra el efecto de cada cargo discutido.' }, { question: '¿Decide si un cargo es legal?', answer: 'No. Solo transforma tus cifras.' }, { question: '¿Qué es un cargo discutido?', answer: 'Una reparación, limpieza, factura u otra cantidad que quieres revisar.' }], howTo: [{ name: 'Introduce la fianza', text: 'Escribe la fianza pagada y elige su moneda.' }, { name: 'Añade los cargos', text: 'Introduce cada cargo con su importe y estado.' }, { name: 'Compara las devoluciones', text: 'Compara la devolución con cada escenario discutido.' }, { name: 'Copia el resumen', text: 'Copia las cifras y revisa el contrato y los justificantes.' }], seoTitle: 'Convierte una liquidación de fianza en una estimación clara', seoText: 'Introduce la fianza y sus cargos para ver la devolución y el efecto de cada disputa.', seoLimit: 'Un cálculo, no una resolución legal' }),
|
|
23
|
+
fr: makeCopy({ title: 'Calculateur de restitution de dépôt de garantie', description: 'Estimez la restitution de votre dépôt après avoir séparé les retenues confirmées et contestées.', ui: { depositLabel: 'Dépôt versé', deductionsTitle: 'Sommes retenues sur le dépôt', descriptionLabel: 'Description', amountLabel: 'Montant', statusLabel: 'Statut de la retenue', confirmedOption: 'Confirmée', disputedOption: 'Contestée', addButton: 'Ajouter une retenue', resetButton: 'Restaurer l\'exemple', currencyLabel: 'Devise', summaryTitle: 'Confirmé ou contesté', estimatedReturnLabel: 'Restitution estimée', undisputedReturnLabel: 'Sans retenues contestées', totalDeductionsLabel: 'Retenues totales', confirmedLabel: 'Retenues confirmées', disputedLabel: 'Retenues contestées', returnFlowTitle: 'Dépôt à restituer', returnNode: 'Restitution estimée', copyButton: 'Copier le résumé', copiedButton: 'Résumé copié', methodTitle: 'Comment fonctionne l\'estimation' }, faq: [{ question: 'Que calcule cet outil ?', answer: 'Il soustrait les retenues saisies du dépôt et montre l\'effet de chaque contestation.' }, { question: 'Décide-t-il si une retenue est légale ?', answer: 'Non. Il transforme uniquement vos montants.' }, { question: 'Qu\'est-ce qu\'une retenue contestée ?', answer: 'Une dépense que vous souhaitez vérifier.' }], howTo: [{ name: 'Saisir le dépôt', text: 'Indiquez le dépôt versé et choisissez sa devise.' }, { name: 'Ajouter les retenues', text: 'Saisissez chaque ligne, son montant et son statut.' }, { name: 'Comparer les restitutions', text: 'Comparez le résultat avec chaque scénario.' }, { name: 'Copier le résumé', text: 'Copiez les chiffres et vérifiez les justificatifs.' }], seoTitle: 'Transformer un décompte de dépôt en estimation', seoText: 'Saisissez le dépôt et les retenues pour voir la restitution et l\'effet de chaque contestation.', seoLimit: 'Un calcul, pas une décision juridique' }),
|
|
24
|
+
id: makeCopy({ title: 'Kalkulator Pengembalian Deposit Sewa', description: 'Perkirakan pengembalian deposit sewa setelah memisahkan potongan yang disetujui dan disengketakan.', ui: { depositLabel: 'Deposit dibayar', deductionsTitle: 'Potongan dari deposit', descriptionLabel: 'Deskripsi', amountLabel: 'Jumlah', statusLabel: 'Status potongan', confirmedOption: 'Disetujui', disputedOption: 'Disengketakan', addButton: 'Tambah potongan', resetButton: 'Pulihkan contoh', currencyLabel: 'Mata uang', summaryTitle: 'Disetujui vs disengketakan', estimatedReturnLabel: 'Perkiraan pengembalian', undisputedReturnLabel: 'Tanpa potongan yang disengketakan', totalDeductionsLabel: 'Total potongan', confirmedLabel: 'Potongan disetujui', disputedLabel: 'Potongan disengketakan', returnFlowTitle: 'Deposit yang dikembalikan', returnNode: 'Perkiraan kembali', copyButton: 'Salin ringkasan', copiedButton: 'Ringkasan disalin', methodTitle: 'Cara kerja perkiraan' }, faq: [{ question: 'Apa yang dihitung kalkulator ini?', answer: 'Kalkulator mengurangi potongan dari deposit dan menunjukkan dampak setiap sengketa.' }, { question: 'Apakah kalkulator menentukan legalitas?', answer: 'Tidak. Kalkulator hanya mengolah angka Anda.' }, { question: 'Apa itu potongan yang disengketakan?', answer: 'Biaya yang ingin Anda periksa.' }], howTo: [{ name: 'Masukkan deposit', text: 'Masukkan deposit dan pilih mata uang.' }, { name: 'Tambahkan potongan', text: 'Masukkan setiap potongan, jumlah, dan statusnya.' }, { name: 'Bandingkan hasil', text: 'Bandingkan pengembalian dengan setiap skenario.' }, { name: 'Salin ringkasan', text: 'Salin angka dan periksa dokumen pendukung.' }], seoTitle: 'Ubah rincian deposit menjadi perkiraan pengembalian', seoText: 'Masukkan deposit dan potongan untuk melihat pengembalian serta dampak setiap sengketa.', seoLimit: 'Perhitungan, bukan keputusan hukum' }),
|
|
25
|
+
it: makeCopy({ title: 'Calcolatore della restituzione del deposito cauzionale', description: 'Stima quanto del deposito potrebbe essere restituito separando trattenute confermate e contestate.', ui: { depositLabel: 'Deposito versato', deductionsTitle: 'Importi trattenuti dal deposito', descriptionLabel: 'Descrizione', amountLabel: 'Importo', statusLabel: 'Stato della trattenuta', confirmedOption: 'Confermata', disputedOption: 'Contestata', addButton: 'Aggiungi trattenuta', resetButton: 'Ripristina esempio', currencyLabel: 'Valuta', summaryTitle: 'Confermate e contestate', estimatedReturnLabel: 'Restituzione stimata', undisputedReturnLabel: 'Senza trattenute contestate', totalDeductionsLabel: 'Trattenute totali', confirmedLabel: 'Trattenute confermate', disputedLabel: 'Trattenute contestate', returnFlowTitle: 'Deposito da restituire', returnNode: 'Restituzione stimata', copyButton: 'Copia riepilogo', copiedButton: 'Riepilogo copiato', methodTitle: 'Come funziona la stima' }, faq: [{ question: 'Cosa stima questo calcolatore?', answer: 'Sottrae le trattenute dal deposito e mostra l\'effetto di ogni contestazione.' }, { question: 'Decide se una trattenuta è legittima?', answer: 'No. Elabora solo i tuoi importi.' }, { question: 'Cos\'è una trattenuta contestata?', answer: 'Una voce che vuoi verificare.' }], howTo: [{ name: 'Inserisci il deposito', text: 'Inserisci il deposito e scegli la valuta.' }, { name: 'Aggiungi le trattenute', text: 'Inserisci ogni voce con importo e stato.' }, { name: 'Confronta i risultati', text: 'Confronta la restituzione con ogni scenario.' }, { name: 'Copia il riepilogo', text: 'Copia i valori e controlla i documenti.' }], seoTitle: 'Trasforma il rendiconto del deposito in una stima', seoText: 'Inserisci deposito e trattenute per vedere la restituzione e l\'effetto di ogni contestazione.', seoLimit: 'Un calcolo, non una decisione legale' }),
|
|
26
|
+
nl: makeCopy({ title: 'Rekenhulp voor terugbetaling van de huurwaarborg', description: 'Schat de terugbetaling van je huurwaarborg na bevestigde en betwiste inhoudingen.', ui: { depositLabel: 'Betaalde waarborg', deductionsTitle: 'Inhoudingen op de waarborg', descriptionLabel: 'Omschrijving', amountLabel: 'Bedrag', statusLabel: 'Status van inhouding', confirmedOption: 'Bevestigd', disputedOption: 'Betwist', addButton: 'Inhouding toevoegen', resetButton: 'Voorbeeld herstellen', currencyLabel: 'Valuta', summaryTitle: 'Bevestigd tegenover betwist', estimatedReturnLabel: 'Geschatte terugbetaling', undisputedReturnLabel: 'Zonder betwiste inhoudingen', totalDeductionsLabel: 'Totale inhoudingen', confirmedLabel: 'Bevestigde inhoudingen', disputedLabel: 'Betwiste inhoudingen', returnFlowTitle: 'Waarborg terug', returnNode: 'Geschat terug', copyButton: 'Samenvatting kopiëren', copiedButton: 'Samenvatting gekopieerd', methodTitle: 'Zo werkt de schatting' }, faq: [{ question: 'Wat schat deze rekentool?', answer: 'De tool trekt inhoudingen af en toont het effect van elke betwiste post.' }, { question: 'Bepaalt de tool of een inhouding wettelijk is?', answer: 'Nee. De tool rekent met je eigen bedragen.' }, { question: 'Wat is een betwiste post?', answer: 'Een bedrag dat je wilt controleren.' }], howTo: [{ name: 'Voer de waarborg in', text: 'Voer de waarborg en valuta in.' }, { name: 'Voeg inhoudingen toe', text: 'Voer elke post met bedrag en status in.' }, { name: 'Vergelijk terugbetalingen', text: 'Bekijk het scenario per betwiste post.' }, { name: 'Kopieer de samenvatting', text: 'Kopieer de bedragen en controleer de stukken.' }], seoTitle: 'Maak van een waarborgafrekening een duidelijke schatting', seoText: 'Voer de waarborg en inhoudingen in om terugbetaling en betwiste posten te vergelijken.', seoLimit: 'Een berekening, geen juridisch oordeel' }),
|
|
27
|
+
pl: makeCopy({ title: 'Kalkulator zwrotu kaucji za wynajem', description: 'Oszacuj zwrot kaucji po rozdzieleniu potrąceń potwierdzonych i kwestionowanych.', ui: { depositLabel: 'Wpłacona kaucja', deductionsTitle: 'Potrącenia z kaucji', descriptionLabel: 'Opis', amountLabel: 'Kwota', statusLabel: 'Status potrącenia', confirmedOption: 'Potwierdzone', disputedOption: 'Kwestionowane', addButton: 'Dodaj potrącenie', resetButton: 'Przywróć przykład', currencyLabel: 'Waluta', summaryTitle: 'Potwierdzone i kwestionowane', estimatedReturnLabel: 'Szacowany zwrot', undisputedReturnLabel: 'Bez kwestionowanych potrąceń', totalDeductionsLabel: 'Suma potrąceń', confirmedLabel: 'Potwierdzone potrącenia', disputedLabel: 'Kwestionowane potrącenia', returnFlowTitle: 'Kaucja do zwrotu', returnNode: 'Szacowany zwrot', copyButton: 'Kopiuj podsumowanie', copiedButton: 'Podsumowanie skopiowane', methodTitle: 'Jak działa szacunek' }, faq: [{ question: 'Co oblicza ten kalkulator?', answer: 'Odejmuje potrącenia od kaucji i pokazuje wpływ każdej spornej pozycji.' }, { question: 'Czy rozstrzyga legalność potrącenia?', answer: 'Nie. Przetwarza podane kwoty.' }, { question: 'Co oznacza kwestionowane potrącenie?', answer: 'Pozycję, którą chcesz sprawdzić.' }], howTo: [{ name: 'Wpisz kaucję', text: 'Wpisz kaucję i wybierz walutę.' }, { name: 'Dodaj potrącenia', text: 'Wpisz każdą pozycję, kwotę i status.' }, { name: 'Porównaj zwroty', text: 'Porównaj wynik z każdym scenariuszem.' }, { name: 'Skopiuj podsumowanie', text: 'Skopiuj liczby i sprawdź dokumenty.' }], seoTitle: 'Zamień rozliczenie kaucji w czytelny szacunek zwrotu', seoText: 'Wpisz kaucję i potrącenia, aby zobaczyć zwrot oraz wpływ każdej spornej pozycji.', seoLimit: 'Obliczenie, nie decyzja prawna' }),
|
|
28
|
+
pt: makeCopy({ title: 'Calculadora de devolução do depósito de arrendamento', description: 'Estime a devolução do seu depósito depois de separar deduções confirmadas e contestadas.', ui: { depositLabel: 'Depósito pago', deductionsTitle: 'Valores retidos do depósito', descriptionLabel: 'Descrição', amountLabel: 'Valor', statusLabel: 'Estado da dedução', confirmedOption: 'Confirmada', disputedOption: 'Contestada', addButton: 'Adicionar dedução', resetButton: 'Restaurar exemplo', currencyLabel: 'Moeda', summaryTitle: 'Confirmadas e contestadas', estimatedReturnLabel: 'Devolução estimada', undisputedReturnLabel: 'Sem deduções contestadas', totalDeductionsLabel: 'Total das deduções', confirmedLabel: 'Deduções confirmadas', disputedLabel: 'Deduções contestadas', returnFlowTitle: 'Depósito a devolver', returnNode: 'Devolução estimada', copyButton: 'Copiar resumo', copiedButton: 'Resumo copiado', methodTitle: 'Como funciona a estimativa' }, faq: [{ question: 'O que estima esta calculadora?', answer: 'Subtrai as deduções do depósito e mostra o efeito de cada contestação.' }, { question: 'Decide se uma dedução é legal?', answer: 'Não. Calcula os seus valores.' }, { question: 'O que é uma dedução contestada?', answer: 'Uma cobrança que pretende verificar.' }], howTo: [{ name: 'Introduza o depósito', text: 'Indique o depósito e escolha a moeda.' }, { name: 'Adicione as deduções', text: 'Introduza cada linha, valor e estado.' }, { name: 'Compare as devoluções', text: 'Compare a devolução com cada cenário.' }, { name: 'Copie o resumo', text: 'Copie os valores e confirme os documentos.' }], seoTitle: 'Transforme o acerto do depósito numa estimativa clara', seoText: 'Introduza o depósito e as deduções para ver a devolução e o efeito de cada contestação.', seoLimit: 'Um cálculo, não uma decisão legal' }),
|
|
29
|
+
tr: makeCopy({ title: 'Kira depozitosu iade hesaplayici', description: 'Onaylanan ve itiraz edilen kesintileri ayırarak kira depozitosu iadesini tahmin edin.', ui: { depositLabel: 'Ödenen depozito', deductionsTitle: 'Depozitodan kesilenler', descriptionLabel: 'Açıklama', amountLabel: 'Tutar', statusLabel: 'Kesinti durumu', confirmedOption: 'Onaylandı', disputedOption: 'İtiraz edildi', addButton: 'Kesinti ekle', resetButton: 'Örneği geri yükle', currencyLabel: 'Para birimi', summaryTitle: 'Onaylanan ve itiraz edilen', estimatedReturnLabel: 'Tahmini iade', undisputedReturnLabel: 'İtiraz edilen kesintiler çıkarılırsa', totalDeductionsLabel: 'Toplam kesinti', confirmedLabel: 'Onaylanan kesintiler', disputedLabel: 'İtiraz edilen kesintiler', returnFlowTitle: 'İade edilecek depozito', returnNode: 'Tahmini iade', copyButton: 'Özeti kopyala', copiedButton: 'Özet kopyalandı', methodTitle: 'Tahmin nasıl çalışır' }, faq: [{ question: 'Bu hesaplayıcı neyi tahmin eder?', answer: 'Kesintileri depozitodan çıkarır ve her itirazın etkisini gösterir.' }, { question: 'Kesintinin yasal olup olmadığını belirler mi?', answer: 'Hayır. Kendi tutarlarınızı hesaplar.' }, { question: 'İtiraz edilen kesinti nedir?', answer: 'Kontrol etmek istediğiniz bir kalemdir.' }], howTo: [{ name: 'Depozitoyu girin', text: 'Depozitoyu ve para birimini seçin.' }, { name: 'Kesintileri ekleyin', text: 'Her kalemin tutarını ve durumunu girin.' }, { name: 'İadeleri karşılaştırın', text: 'Her senaryoyu karşılaştırın.' }, { name: 'Özeti kopyalayın', text: 'Tutarları kopyalayın ve belgeleri kontrol edin.' }], seoTitle: 'Depozito dökümünü anlaşılır bir iade tahminine dönüştürün', seoText: 'Depozito ve kesintileri girerek iade ile her itirazın etkisini görün.', seoLimit: 'Hesaplamadır, hukuki karar değildir' }),
|
|
30
|
+
ja: makeCopy({ title: '\u8cc3\u8cb8\u4fdd\u8a3c\u91d1\u8fd4\u9084\u8a08\u7b97\u6a5f', description: '\u78ba\u5b9a\u3057\u305f\u63a7\u9664\u3068\u4e89\u3063\u3066\u3044\u308b\u63a7\u9664\u3092\u5206\u3051\u3066\u3001\u8cc3\u8cb8\u4fdd\u8a3c\u91d1\u306e\u8fd4\u9084\u984d\u3092\u898b\u7a4d\u3082\u308a\u307e\u3059\u3002', ui: { depositLabel: '\u652f\u6255\u3063\u305f\u4fdd\u8a3c\u91d1', deductionsTitle: '\u4fdd\u8a3c\u91d1\u304b\u3089\u5dee\u3057\u5f15\u304b\u308c\u305f\u9805\u76ee', descriptionLabel: '\u5185\u5bb9', amountLabel: '\u91d1\u984d', statusLabel: '\u63a7\u9664\u306e\u72b6\u614b', confirmedOption: '\u78ba\u5b9a', disputedOption: '\u4e89\u8b70\u4e2d', addButton: '\u63a7\u9664\u3092\u8ffd\u52a0', resetButton: '\u4f8b\u3092\u5fa9\u5143', currencyLabel: '\u901a\u8ca8', summaryTitle: '\u78ba\u5b9a\u3068\u4e89\u8b70\u4e2d', estimatedReturnLabel: '\u8fd4\u9084\u898b\u8fbc\u307f', undisputedReturnLabel: '\u4e89\u8b70\u4e2d\u306e\u63a7\u9664\u3092\u9664\u3044\u305f\u5834\u5408', totalDeductionsLabel: '\u63a7\u9664\u5408\u8a08', confirmedLabel: '\u78ba\u5b9a\u3057\u305f\u63a7\u9664', disputedLabel: '\u4e89\u8b70\u4e2d\u306e\u63a7\u9664', returnFlowTitle: '\u8fd4\u9084\u3055\u308c\u308b\u4fdd\u8a3c\u91d1', returnNode: '\u8fd4\u9084\u898b\u8fbc\u307f', copyButton: '\u6982\u8981\u3092\u30b3\u30d4\u30fc', copiedButton: '\u6982\u8981\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3057\u305f', methodTitle: '\u8a08\u7b97\u65b9\u6cd5' }, faq: [{ question: '\u3053\u306e\u8a08\u7b97\u6a5f\u306f\u4f55\u3092\u898b\u7a4d\u3082\u308a\u307e\u3059\u304b\uff1f', answer: '\u4fdd\u8a3c\u91d1\u304b\u3089\u63a7\u9664\u3092\u5dee\u3057\u5f15\u304d\u3001\u4e89\u8b70\u4e2d\u306e\u9805\u76ee\u306e\u5f71\u97ff\u3092\u8868\u793a\u3057\u307e\u3059\u3002' }, { question: '\u6cd5\u7684\u306a\u5224\u65ad\u3067\u3059\u304b\uff1f', answer: '\u3044\u3048\u3001\u91d1\u984d\u306e\u8a08\u7b97\u3067\u3059\u3002' }, { question: '\u4e89\u8b70\u4e2d\u306e\u9805\u76ee\u3068\u306f\u4f55\u3067\u3059\u304b\uff1f', answer: '\u78ba\u8a8d\u3057\u305f\u3044\u8acb\u6c42\u3067\u3059\u3002' }], howTo: [{ name: '\u4fdd\u8a3c\u91d1\u3092\u5165\u529b', text: '\u4fdd\u8a3c\u91d1\u3068\u901a\u8ca8\u3092\u9078\u3073\u307e\u3059\u3002' }, { name: '\u63a7\u9664\u3092\u8ffd\u52a0', text: '\u5404\u9805\u76ee\u306e\u91d1\u984d\u3068\u72b6\u614b\u3092\u5165\u529b\u3057\u307e\u3059\u3002' }, { name: '\u8fd4\u9084\u984d\u3092\u6bd4\u8f03', text: '\u5404\u30b7\u30ca\u30ea\u30aa\u3092\u6bd4\u8f03\u3057\u307e\u3059\u3002' }, { name: '\u6982\u8981\u3092\u30b3\u30d4\u30fc', text: '\u6570\u5024\u3092\u30e1\u30e2\u306b\u30b3\u30d4\u30fc\u3057\u307e\u3059\u3002' }], seoTitle: '\u4fdd\u8a3c\u91d1\u306e\u660e\u7d30\u3092\u8fd4\u9084\u984d\u306e\u898b\u7a4d\u3082\u308a\u306b\u5909\u3048\u308b', seoText: '\u4fdd\u8a3c\u91d1\u3068\u63a7\u9664\u306e\u660e\u7d30\u3092\u5165\u529b\u3057\u3001\u8fd4\u9084\u984d\u3068\u5404\u9805\u76ee\u306e\u5f71\u97ff\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002', seoLimit: '\u6cd5\u5f8b\u306e\u5224\u65ad\u3067\u306f\u306a\u304f\u8a08\u7b97\u3067\u3059' }),
|
|
31
|
+
ko: makeCopy({ title: '\uc784\ub300 \ubcf4\uc99d\uae08 \ubc18\ud658 \uacc4\uc0b0\uae30', description: '\ud655\uc815 \uacf5\uc81c\uc640 \uc774\uc758 \uc81c\uae30 \uacf5\uc81c\ub97c \ub098\ub204\uc5b4 \uc784\ub300 \ubcf4\uc99d\uae08 \ubc18\ud658\uc561\uc744 \uc608\uc0c1\ud569\ub2c8\ub2e4.', ui: { depositLabel: '\ub0a9\ubd80\ud55c \ubcf4\uc99d\uae08', deductionsTitle: '\ubcf4\uc99d\uae08\uc5d0\uc11c \uacf5\uc81c\ub41c \ud56d\ubaa9', descriptionLabel: '\ub0b4\uc6a9', amountLabel: '\uae08\uc561', statusLabel: '\uacf5\uc81c \uc0c1\ud0dc', confirmedOption: '\ud655\uc815', disputedOption: '\uc774\uc758 \uc81c\uae30', addButton: '\uacf5\uc81c \ucd94\uac00', resetButton: '\uc608\uc2dc \ubcf5\uc6d0', currencyLabel: '\ud1b5\ud654', summaryTitle: '\ud655\uc815\uacfc \uc774\uc758 \uc81c\uae30', estimatedReturnLabel: '\uc608\uc0c1 \ubc18\ud658\uc561', undisputedReturnLabel: '\uc774\uc758 \uc81c\uae30 \uacf5\uc81c \uc81c\uc678 \uc2dc', totalDeductionsLabel: '\ucd1d \uacf5\uc81c\uc561', confirmedLabel: '\ud655\uc815 \uacf5\uc81c', disputedLabel: '\uc774\uc758 \uc81c\uae30 \uacf5\uc81c', returnFlowTitle: '\ubc18\ud658\ub420 \ubcf4\uc99d\uae08', returnNode: '\uc608\uc0c1 \ubc18\ud658', copyButton: '\uc694\uc57d \ubcf5\uc0ac', copiedButton: '\uc694\uc57d\uc774 \ubcf5\uc0ac\ub418\uc5c8\uc2b5\ub2c8\ub2e4', methodTitle: '\uacc4\uc0b0 \ubc29\ubc95' }, faq: [{ question: '\uc774 \uacc4\uc0b0\uae30\ub294 \ubb34\uc5c7\uc744 \uc608\uc0c1\ud558\ub098\uc694?', answer: '\ubcf4\uc99d\uae08\uc5d0\uc11c \uacf5\uc81c\uc561\uc744 \ube7c\uace0 \uc774\uc758 \uc81c\uae30 \ud56d\ubaa9\uc758 \uc601\ud5a5\uc744 \ubcf4\uc5ec\uc90d\ub2c8\ub2e4.' }, { question: '\ubc95\uc801 \ud310\ub2e8\uc744 \ud558\ub098\uc694?', answer: '\uc544\ub2c8\uc694. \uc785\ub825\ud55c \uae08\uc561\ub9cc \uacc4\uc0b0\ud569\ub2c8\ub2e4.' }, { question: '\uc774\uc758 \uc81c\uae30 \ud56d\ubaa9\uc774\ub780 \ubb34\uc5c7\uc778\uac00\uc694?', answer: '\ud655\uc778\ud558\uace0 \uc2f6\uc740 \uacf5\uc81c \ud56d\ubaa9\uc785\ub2c8\ub2e4.' }], howTo: [{ name: '\ubcf4\uc99d\uae08 \uc785\ub825', text: '\ubcf4\uc99d\uae08\uacfc \ud1b5\ud654\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4.' }, { name: '\uacf5\uc81c \ucd94\uac00', text: '\uac01 \ud56d\ubaa9\uc758 \uae08\uc561\uacfc \uc0c1\ud0dc\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.' }, { name: '\ubc18\ud658\uc561 \ube44\uad50', text: '\uac01 \uc2dc\ub098\ub9ac\uc624\ub97c \ube44\uad50\ud569\ub2c8\ub2e4.' }, { name: '\uc694\uc57d \ubcf5\uc0ac', text: '\uc218\uce58\ub97c \uba54\ubaa8\uc5d0 \ubcf5\uc0ac\ud569\ub2c8\ub2e4.' }], seoTitle: '\ubcf4\uc99d\uae08 \uba85\uc138\ub97c \ubc18\ud658\uc561 \uc608\uc0c1\uc73c\ub85c \ubc14\uafb8\uae30', seoText: '\ubcf4\uc99d\uae08\uacfc \uacf5\uc81c \ub0b4\uc5ed\uc744 \uc785\ub825\ud558\uba74 \uc608\uc0c1 \ubc18\ud658\uc561\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.', seoLimit: '\ubc95\uc801 \ud310\ub2e8\uc774 \uc544\ub2cc \uacc4\uc0b0' }),
|
|
32
|
+
ru: makeCopy({ title: '\u041a\u0430\u043b\u044c\u043a\u0443\u043b\u044f\u0442\u043e\u0440 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u0430\u0440\u0435\u043d\u0434\u043d\u043e\u0433\u043e \u0437\u0430\u043b\u043e\u0433\u0430', description: '\u041e\u0446\u0435\u043d\u0438\u0442\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0437\u0430\u043b\u043e\u0433\u0430, \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0432 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043d\u043d\u044b\u0435 \u0438 \u043e\u0441\u043f\u0430\u0440\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f.', ui: { depositLabel: '\u0412\u043d\u0435\u0441\u0451\u043d\u043d\u044b\u0439 \u0437\u0430\u043b\u043e\u0433', deductionsTitle: '\u0423\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u0438\u0437 \u0437\u0430\u043b\u043e\u0433\u0430', descriptionLabel: '\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435', amountLabel: '\u0421\u0443\u043c\u043c\u0430', statusLabel: '\u0421\u0442\u0430\u0442\u0443\u0441 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f', confirmedOption: '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e', disputedOption: '\u041e\u0441\u043f\u0430\u0440\u0438\u0432\u0430\u0435\u0442\u0441\u044f', addButton: '\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435', resetButton: '\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u0440', currencyLabel: '\u0412\u0430\u043b\u044e\u0442\u0430', summaryTitle: '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u0438 \u043e\u0441\u043f\u0430\u0440\u0438\u0432\u0430\u0435\u0442\u0441\u044f', estimatedReturnLabel: '\u0420\u0430\u0441\u0447\u0451\u0442\u043d\u044b\u0439 \u0432\u043e\u0437\u0432\u0440\u0430\u0442', undisputedReturnLabel: '\u0411\u0435\u0437 \u043e\u0441\u043f\u0430\u0440\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0439', totalDeductionsLabel: '\u0412\u0441\u0435 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f', confirmedLabel: '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043d\u043d\u044b\u0435', disputedLabel: '\u041e\u0441\u043f\u0430\u0440\u0438\u0432\u0430\u0435\u043c\u044b\u0435', returnFlowTitle: '\u0417\u0430\u043b\u043e\u0433 \u043a \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0443', returnNode: '\u0420\u0430\u0441\u0447\u0451\u0442\u043d\u044b\u0439 \u0432\u043e\u0437\u0432\u0440\u0430\u0442', copyButton: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0434\u043a\u0443', copiedButton: '\u0421\u0432\u043e\u0434\u043a\u0430 \u0441\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0430', methodTitle: '\u041a\u0430\u043a \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0440\u0430\u0441\u0447\u0451\u0442' }, faq: [{ question: '\u0427\u0442\u043e \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442?', answer: '\u041e\u043d \u0432\u044b\u0447\u0438\u0442\u0430\u0435\u0442 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u0438\u0437 \u0437\u0430\u043b\u043e\u0433\u0430.' }, { question: '\u042d\u0442\u043e \u044e\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435?', answer: '\u041d\u0435\u0442, \u044d\u0442\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0441\u0447\u0451\u0442.' }, { question: '\u0427\u0442\u043e \u0442\u0430\u043a\u043e\u0435 \u0441\u043f\u043e\u0440\u043d\u043e\u0435 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435?', answer: '\u0421\u0443\u043c\u043c\u0430, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c.' }], howTo: [{ name: '\u0412\u0432\u0435\u0441\u0442\u0438 \u0437\u0430\u043b\u043e\u0433', text: '\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0437\u0430\u043b\u043e\u0433 \u0438 \u0432\u0430\u043b\u044e\u0442\u0443.' }, { name: '\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f', text: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0443\u043c\u043c\u044b \u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u044b.' }, { name: '\u0421\u0440\u0430\u0432\u043d\u0438\u0442\u044c \u0432\u043e\u0437\u0432\u0440\u0430\u0442', text: '\u0421\u0440\u0430\u0432\u043d\u0438\u0442\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438.' }, { name: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0434\u043a\u0443', text: '\u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0447\u0438\u0441\u043b\u0430.' }], seoTitle: '\u041f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u0442\u0435 \u0440\u0430\u0441\u0447\u0451\u0442 \u0437\u0430\u043b\u043e\u0433\u0430 \u0432 \u043f\u043e\u043d\u044f\u0442\u043d\u0443\u044e \u043e\u0446\u0435\u043d\u043a\u0443', seoText: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u0430\u043b\u043e\u0433 \u0438 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0438 \u0432\u043b\u0438\u044f\u043d\u0438\u0435 \u0441\u043f\u043e\u0440\u043e\u0432.', seoLimit: '\u0420\u0430\u0441\u0447\u0451\u0442, \u0430 \u043d\u0435 \u044e\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435' }),
|
|
33
|
+
zh: makeCopy({ title: '\u79df\u8d41\u62bc\u91d1\u9000\u8fd8\u8ba1\u7b97\u5668', description: '\u533a\u5206\u5df2\u786e\u8ba4\u548c\u6709\u4e89\u8bae\u7684\u6263\u6b3e\uff0c\u4f30\u7b97\u79df\u8d41\u62bc\u91d1\u9000\u8fd8\u91d1\u989d\u3002', ui: { depositLabel: '\u5df2\u4ed8\u62bc\u91d1', deductionsTitle: '\u4ece\u62bc\u91d1\u4e2d\u6263\u9664\u7684\u9879\u76ee', descriptionLabel: '\u8bf4\u660e', amountLabel: '\u91d1\u989d', statusLabel: '\u6263\u6b3e\u72b6\u6001', confirmedOption: '\u5df2\u786e\u8ba4', disputedOption: '\u6709\u4e89\u8bae', addButton: '\u6dfb\u52a0\u6263\u6b3e', resetButton: '\u6062\u590d\u793a\u4f8b', currencyLabel: '\u8d27\u5e01', summaryTitle: '\u5df2\u786e\u8ba4\u4e0e\u6709\u4e89\u8bae', estimatedReturnLabel: '\u9884\u8ba1\u9000\u8fd8', undisputedReturnLabel: '\u79fb\u9664\u6709\u4e89\u8bae\u6263\u6b3e\u540e', totalDeductionsLabel: '\u6263\u6b3e\u603b\u989d', confirmedLabel: '\u5df2\u786e\u8ba4\u6263\u6b3e', disputedLabel: '\u6709\u4e89\u8bae\u6263\u6b3e', returnFlowTitle: '\u53ef\u9000\u62bc\u91d1', returnNode: '\u9884\u8ba1\u9000\u8fd8', copyButton: '\u590d\u5236\u6458\u8981', copiedButton: '\u6458\u8981\u5df2\u590d\u5236', methodTitle: '\u8ba1\u7b97\u65b9\u5f0f' }, faq: [{ question: '\u8fd9\u4e2a\u8ba1\u7b97\u5668\u4f30\u7b97\u4ec0\u4e48\uff1f', answer: '\u4ece\u62bc\u91d1\u4e2d\u51cf\u53bb\u6263\u6b3e\uff0c\u5e76\u663e\u793a\u6bcf\u9879\u4e89\u8bae\u7684\u5f71\u54cd\u3002' }, { question: '\u4f1a\u5224\u65ad\u6263\u6b3e\u662f\u5426\u5408\u6cd5\u5417\uff1f', answer: '\u4e0d\u4f1a\uff0c\u53ea\u8ba1\u7b97\u4f60\u8f93\u5165\u7684\u91d1\u989d\u3002' }, { question: '\u4ec0\u4e48\u662f\u6709\u4e89\u8bae\u7684\u6263\u6b3e\uff1f', answer: '\u8fd9\u662f\u4f60\u60f3\u6838\u67e5\u7684\u9879\u76ee\u3002' }], howTo: [{ name: '\u8f93\u5165\u62bc\u91d1', text: '\u8f93\u5165\u62bc\u91d1\u5e76\u9009\u62e9\u8d27\u5e01\u3002' }, { name: '\u6dfb\u52a0\u6263\u6b3e', text: '\u8f93\u5165\u6bcf\u4e2a\u9879\u76ee\u7684\u91d1\u989d\u548c\u72b6\u6001\u3002' }, { name: '\u6bd4\u8f83\u9000\u8fd8\u91d1\u989d', text: '\u6bd4\u8f83\u9884\u8ba1\u9000\u8fd8\u548c\u6bcf\u4e2a\u4e89\u8bae\u573a\u666f\u3002' }, { name: '\u590d\u5236\u6458\u8981', text: '\u590d\u5236\u91d1\u989d\u5e76\u68c0\u67e5\u8bc1\u660e\u6750\u6599\u3002' }], seoTitle: '\u628a\u62bc\u91d1\u660e\u7ec6\u53d8\u6210\u6e05\u6670\u7684\u9000\u8fd8\u4f30\u7b97', seoText: '\u8f93\u5165\u62bc\u91d1\u548c\u6263\u6b3e\u5373\u53ef\u770b\u5230\u9884\u8ba1\u9000\u8fd8\u4ee5\u53ca\u6bcf\u9879\u4e89\u8bae\u7684\u5f71\u54cd\u3002', seoLimit: '\u8fd9\u662f\u8ba1\u7b97\uff0c\u4e0d\u662f\u6cd5\u5f8b\u5224\u65ad' }),
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { rentalDepositReturn } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const RENTAL_DEPOSIT_RETURN_TOOL: ToolDefinition = {
|
|
7
|
+
entry: rentalDepositReturn,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CURRENCIES, getDefaultCurrency } from './currency';
|
|
3
|
+
import { calculateRentalDepositReturn, type DeductionInput } from './logic';
|
|
4
|
+
|
|
5
|
+
const deductions: DeductionInput[] = [
|
|
6
|
+
{ id: 'bills', description: 'Final bills', amount: 84, status: 'confirmed' },
|
|
7
|
+
{ id: 'repair', description: 'Wall repair', amount: 165, status: 'disputed' },
|
|
8
|
+
{ id: 'cleaning', description: 'Cleaning', amount: 60, status: 'disputed' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe('calculateRentalDepositReturn', () => {
|
|
12
|
+
it('supports the shared finance currency set and locale defaults', () => {
|
|
13
|
+
expect(Object.keys(CURRENCIES)).toHaveLength(20);
|
|
14
|
+
expect(getDefaultCurrency('en')).toBe('USD');
|
|
15
|
+
expect(getDefaultCurrency('es')).toBe('EUR');
|
|
16
|
+
expect(getDefaultCurrency('en-GB')).toBe('GBP');
|
|
17
|
+
expect(getDefaultCurrency('ja')).toBe('JPY');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('separates the estimated return from the return without disputed deductions', () => {
|
|
21
|
+
const result = calculateRentalDepositReturn({ currency: 'eur', deposit: 1200, deductions });
|
|
22
|
+
expect(result).toMatchObject({
|
|
23
|
+
totalDeductions: 309,
|
|
24
|
+
confirmedDeductions: 84,
|
|
25
|
+
disputedDeductions: 225,
|
|
26
|
+
estimatedReturn: 891,
|
|
27
|
+
undisputedReturn: 1116,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('creates a scenario for every disputed deduction', () => {
|
|
32
|
+
const result = calculateRentalDepositReturn({ currency: 'USD', deposit: 1200, deductions });
|
|
33
|
+
expect(result?.scenarios).toEqual([
|
|
34
|
+
{ deductionId: 'repair', description: 'Wall repair', returnAmount: 1056, additionalReturn: 165 },
|
|
35
|
+
{ deductionId: 'cleaning', description: 'Cleaning', returnAmount: 951, additionalReturn: 60 },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not return a negative balance and flags deductions above the deposit', () => {
|
|
40
|
+
const result = calculateRentalDepositReturn({
|
|
41
|
+
currency: 'GBP',
|
|
42
|
+
deposit: 100,
|
|
43
|
+
deductions: [{ id: 'repair', description: 'Repair', amount: 140, status: 'confirmed' }],
|
|
44
|
+
});
|
|
45
|
+
expect(result).toMatchObject({ estimatedReturn: 0, undisputedReturn: 0, overDeducted: 40 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects invalid currency, deposit and deduction data', () => {
|
|
49
|
+
expect(calculateRentalDepositReturn({ currency: 'EURO', deposit: 100, deductions })).toBeNull();
|
|
50
|
+
expect(calculateRentalDepositReturn({ currency: 'EUR', deposit: -1, deductions })).toBeNull();
|
|
51
|
+
expect(calculateRentalDepositReturn({ currency: 'EUR', deposit: 100, deductions: [{ ...deductions[0]!, amount: -1 }] })).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isCurrencyCode, type CurrencyCode } from './currency';
|
|
2
|
+
|
|
3
|
+
export type DeductionStatus = 'confirmed' | 'disputed';
|
|
4
|
+
|
|
5
|
+
export interface DeductionInput {
|
|
6
|
+
id: string;
|
|
7
|
+
description: string;
|
|
8
|
+
amount: number;
|
|
9
|
+
status: DeductionStatus;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RentalDepositInput {
|
|
13
|
+
currency: string;
|
|
14
|
+
deposit: number;
|
|
15
|
+
deductions: DeductionInput[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DeductionResult extends DeductionInput {
|
|
19
|
+
shareOfDeposit: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DisputedScenario {
|
|
23
|
+
deductionId: string;
|
|
24
|
+
description: string;
|
|
25
|
+
returnAmount: number;
|
|
26
|
+
additionalReturn: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RentalDepositResult {
|
|
30
|
+
currency: CurrencyCode;
|
|
31
|
+
deposit: number;
|
|
32
|
+
deductions: DeductionResult[];
|
|
33
|
+
totalDeductions: number;
|
|
34
|
+
confirmedDeductions: number;
|
|
35
|
+
disputedDeductions: number;
|
|
36
|
+
estimatedReturn: number;
|
|
37
|
+
undisputedReturn: number;
|
|
38
|
+
overDeducted: number;
|
|
39
|
+
scenarios: DisputedScenario[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const roundMoney = (value: number): number => Number(value.toFixed(2));
|
|
43
|
+
|
|
44
|
+
const clampReturn = (deposit: number, deductions: number): number =>
|
|
45
|
+
roundMoney(Math.max(0, deposit - deductions));
|
|
46
|
+
|
|
47
|
+
const isStatus = (value: string): value is DeductionStatus =>
|
|
48
|
+
value === 'confirmed' || value === 'disputed';
|
|
49
|
+
|
|
50
|
+
const isValidDeduction = (deduction: DeductionInput): boolean =>
|
|
51
|
+
deduction.id.trim().length > 0
|
|
52
|
+
&& deduction.description.trim().length > 0
|
|
53
|
+
&& Number.isFinite(deduction.amount)
|
|
54
|
+
&& deduction.amount >= 0
|
|
55
|
+
&& isStatus(deduction.status);
|
|
56
|
+
|
|
57
|
+
function summarizeDeductions(input: RentalDepositInput): { total: number; confirmed: number; disputed: number } {
|
|
58
|
+
const total = roundMoney(input.deductions.reduce((sum, deduction) => sum + deduction.amount, 0));
|
|
59
|
+
const confirmed = roundMoney(input.deductions
|
|
60
|
+
.filter((deduction) => deduction.status === 'confirmed')
|
|
61
|
+
.reduce((sum, deduction) => sum + deduction.amount, 0));
|
|
62
|
+
return { total, confirmed, disputed: roundMoney(total - confirmed) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addShares(input: RentalDepositInput): DeductionResult[] {
|
|
66
|
+
return input.deductions.map((deduction) => ({
|
|
67
|
+
...deduction,
|
|
68
|
+
shareOfDeposit: input.deposit > 0 ? roundMoney((deduction.amount / input.deposit) * 100) : 0,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildScenarios(input: RentalDepositInput, totalDeductions: number, estimatedReturn: number): DisputedScenario[] {
|
|
73
|
+
return input.deductions
|
|
74
|
+
.filter((deduction) => deduction.status === 'disputed')
|
|
75
|
+
.map((deduction) => {
|
|
76
|
+
const returnAmount = clampReturn(input.deposit, totalDeductions - deduction.amount);
|
|
77
|
+
return {
|
|
78
|
+
deductionId: deduction.id,
|
|
79
|
+
description: deduction.description,
|
|
80
|
+
returnAmount,
|
|
81
|
+
additionalReturn: roundMoney(returnAmount - estimatedReturn),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function calculateRentalDepositReturn(input: RentalDepositInput): RentalDepositResult | null {
|
|
87
|
+
const currency = input.currency.trim().toUpperCase();
|
|
88
|
+
if (!isCurrencyCode(currency) || !Number.isFinite(input.deposit) || input.deposit < 0) return null;
|
|
89
|
+
if (input.deductions.some((deduction) => !isValidDeduction(deduction))) return null;
|
|
90
|
+
const { total: totalDeductions, confirmed: confirmedDeductions, disputed: disputedDeductions } = summarizeDeductions(input);
|
|
91
|
+
const estimatedReturn = clampReturn(input.deposit, totalDeductions);
|
|
92
|
+
const undisputedReturn = clampReturn(input.deposit, confirmedDeductions);
|
|
93
|
+
const deductions = addShares(input);
|
|
94
|
+
const scenarios = buildScenarios(input, totalDeductions, estimatedReturn);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
currency,
|
|
98
|
+
deposit: roundMoney(input.deposit),
|
|
99
|
+
deductions,
|
|
100
|
+
totalDeductions,
|
|
101
|
+
confirmedDeductions,
|
|
102
|
+
disputedDeductions,
|
|
103
|
+
estimatedReturn,
|
|
104
|
+
undisputedReturn,
|
|
105
|
+
overDeducted: roundMoney(Math.max(0, totalDeductions - input.deposit)),
|
|
106
|
+
scenarios,
|
|
107
|
+
};
|
|
108
|
+
}
|