@jjlmoya/utils-finance 1.43.0 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/insuranceDeductibleBreakEvenCalculator/bibliography.astro +10 -0
  7. package/src/tool/insuranceDeductibleBreakEvenCalculator/bibliography.ts +6 -0
  8. package/src/tool/insuranceDeductibleBreakEvenCalculator/component.astro +172 -0
  9. package/src/tool/insuranceDeductibleBreakEvenCalculator/currency.test.ts +13 -0
  10. package/src/tool/insuranceDeductibleBreakEvenCalculator/currency.ts +26 -0
  11. package/src/tool/insuranceDeductibleBreakEvenCalculator/entry.ts +28 -0
  12. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/content-factory.ts +147 -0
  13. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/de.ts +2 -0
  14. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/en.ts +100 -0
  15. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/es.ts +2 -0
  16. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/fr.ts +2 -0
  17. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/id.ts +2 -0
  18. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/it.ts +2 -0
  19. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/ja.ts +2 -0
  20. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/ko.ts +2 -0
  21. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/nl.ts +2 -0
  22. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/pl.ts +2 -0
  23. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/pt.ts +2 -0
  24. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/ru.ts +2 -0
  25. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/sv.ts +2 -0
  26. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/tr.ts +2 -0
  27. package/src/tool/insuranceDeductibleBreakEvenCalculator/i18n/zh.ts +2 -0
  28. package/src/tool/insuranceDeductibleBreakEvenCalculator/index.ts +11 -0
  29. package/src/tool/insuranceDeductibleBreakEvenCalculator/insurance-deductible-break-even-calculator.css +424 -0
  30. package/src/tool/insuranceDeductibleBreakEvenCalculator/logic.test.ts +40 -0
  31. package/src/tool/insuranceDeductibleBreakEvenCalculator/logic.ts +72 -0
  32. package/src/tool/insuranceDeductibleBreakEvenCalculator/seo.astro +11 -0
  33. package/src/tool/insuranceDeductibleBreakEvenCalculator/ui.ts +33 -0
  34. package/src/tools.ts +2 -1
@@ -0,0 +1,100 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { InsuranceDeductibleBreakEvenCalculatorUI } from '../ui';
4
+
5
+ export const ui: InsuranceDeductibleBreakEvenCalculatorUI = {
6
+ labelDamageCost: 'Estimated damage cost',
7
+ labelDeductible: 'Policy deductible',
8
+ labelAnnualPremium: 'Current annual premium',
9
+ labelPremiumIncrease: 'Possible premium increase',
10
+ labelSurchargeYears: 'Years with the increase',
11
+ labelCurrency: 'Display currency',
12
+ labelClaimRoute: 'File a claim',
13
+ labelPayYourselfRoute: 'Pay the damage yourself',
14
+ labelClaimCost: 'Your estimated claim cost',
15
+ labelPayYourselfCost: 'Your estimated self-pay cost',
16
+ labelPremiumSurcharge: 'Estimated premium surcharge',
17
+ labelInsurerShare: 'Amount above the deductible',
18
+ labelBreakEvenDamage: 'Break-even damage amount',
19
+ labelDifference: 'Potential saving by choosing the cheaper route',
20
+ labelRecommendation: 'Model recommendation',
21
+ labelClaimRecommended: 'Filing may cost less',
22
+ labelPayYourselfRecommended: 'Paying yourself may cost less',
23
+ labelBreakEvenRecommendation: 'Both routes cost about the same',
24
+ labelVisual: 'Comparison of estimated costs for filing a claim and paying the damage yourself',
25
+ labelReset: 'Reset example',
26
+ labelCopy: 'Copy summary',
27
+ labelCopied: 'Summary copied',
28
+ labelCopyFailed: 'Copy it manually from the result',
29
+ labelInvalidInput: 'Check the highlighted value',
30
+ labelMethodTitle: 'How this estimate works',
31
+ labelMethodText: 'The model treats the claim route as the deductible, or the full damage when the damage is below the deductible, plus the extra premium caused by the increase you enter. It compares that amount with paying the full damage yourself. The break-even damage amount is the deductible plus the estimated total surcharge. Check that your policy covers this type of loss and ask your insurer how a claim could affect your premium.',
32
+ labelPrivacyNote: 'Your figures stay in this browser. No account, API or server calculation is required.',
33
+ labelAmountPlaceholder: '0.00',
34
+ labelPercentPlaceholder: '0 to 100',
35
+ labelYearsPlaceholder: '0.5 to 20',
36
+ labelApproximateRates: 'Currency changes use approximate planning factors, not live exchange rates.',
37
+ };
38
+
39
+ const slug = 'insurance-deductible-break-even-calculator';
40
+ const title = 'Insurance Deductible Break Even Calculator';
41
+ const description = 'Compare filing an insurance claim with paying for a loss yourself using your damage cost, deductible, premium and possible surcharge. This private browser estimate helps you see the break-even amount without predicting your insurer\'s decision.';
42
+
43
+ const faq = [
44
+ {
45
+ question: 'What does this insurance deductible calculator compare?',
46
+ answer: 'It compares two user-defined routes for one loss: the estimated out-of-pocket cost of filing a claim and the full cost of paying for the damage yourself. The claim route includes the deductible and the possible premium surcharge you enter.',
47
+ },
48
+ {
49
+ question: 'What is the break-even damage amount?',
50
+ answer: 'In this model, it is the deductible plus the estimated total premium surcharge. Damage above that amount may make a claim cheaper, while damage below it may make self-payment cheaper. The result assumes the loss is covered and the insurer pays the eligible amount above the deductible.',
51
+ },
52
+ {
53
+ question: 'Can this calculator predict whether my premium will increase?',
54
+ answer: 'No. You provide the possible percentage and the number of years for the estimate. Your insurer, policy, claim history, location and type of loss determine what actually happens.',
55
+ },
56
+ {
57
+ question: 'What if the damage is smaller than my deductible?',
58
+ answer: 'The model shows no insurer contribution because the damage does not exceed the deductible. Filing may still have policy or reporting implications, so check your policy and ask the insurer before deciding.',
59
+ },
60
+ {
61
+ question: 'Is the result insurance or legal advice?',
62
+ answer: 'No. It is a transparent comparison using your own assumptions. It does not check coverage, depreciation, claim limits, taxes, reporting deadlines or local rules, and it does not recommend concealing a loss or delaying a required notice.',
63
+ },
64
+ ];
65
+
66
+ const howTo = [
67
+ { name: 'Enter the estimated damage', text: 'Use the repair, replacement or loss amount you are currently considering, in the currency shown.' },
68
+ { name: 'Add the policy deductible', text: 'Enter the deductible that applies to this type of claim, rather than a different deductible from another part of the policy.' },
69
+ { name: 'Test the premium effect', text: 'Enter a possible percentage increase and how many years you want to include. Use zero when you want to see the deductible-only comparison.' },
70
+ { name: 'Read the decision boundary', text: 'Compare the two estimated costs and the break-even damage amount, then confirm coverage and any reporting requirement with your insurer.' },
71
+ ];
72
+
73
+ export const content: ToolLocaleContent<InsuranceDeductibleBreakEvenCalculatorUI> = {
74
+ slug,
75
+ title,
76
+ description,
77
+ ui,
78
+ seo: [
79
+ { type: 'title', text: 'Should you file a claim or pay for the damage?', level: 2 },
80
+ { type: 'paragraph', html: '<p>A small loss can create an awkward insurance decision. The deductible is only part of the picture if you expect a claim to affect future premiums. This calculator puts your own damage estimate, deductible and possible surcharge into one transparent comparison so you can see the cost boundary before contacting the insurer.</p>' },
81
+ { type: 'title', text: 'What you need to enter', level: 2 },
82
+ { type: 'list', items: ['The estimated cost of the damage or repair.', 'The deductible shown for this coverage in your policy.', 'Your current annual premium.', 'A possible premium increase and how long you want to model it.'] },
83
+ { type: 'title', text: 'How to interpret the result', level: 2 },
84
+ { type: 'paragraph', html: '<p>The claim estimate combines the deductible with the total surcharge you entered. The self-pay estimate is the full damage cost. The break-even amount shows the loss size at which those two simplified routes meet. A recommendation is only a cost comparison: it does not establish coverage, claim eligibility or the insurer\'s actual premium decision.</p>' },
85
+ { type: 'comparative', columns: 2, items: [
86
+ { title: 'File a claim', description: 'Useful when the covered amount above the deductible outweighs the possible surcharge.', points: ['Pays the deductible in the model', 'Includes the entered premium effect', 'Shows the estimated insurer contribution'] },
87
+ { title: 'Pay yourself', description: 'Useful when the loss is close to or below the modelled break-even amount.', points: ['Avoids the modelled surcharge', 'Pays the full damage estimate', 'May avoid a claim process, subject to your policy'] },
88
+ ] },
89
+ { type: 'title', text: 'Limits and privacy', level: 2 },
90
+ { type: 'paragraph', html: '<p>This is not a claim decision engine. It does not read your policy, check whether the loss is covered, estimate depreciation or predict a premium. It assumes a simple covered loss where the insurer pays eligible costs above the deductible. All figures are processed in your browser, so you can test private scenarios without uploading them.</p>' },
91
+ ],
92
+ faq,
93
+ bibliography,
94
+ howTo,
95
+ schemas: [
96
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
97
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) },
98
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'FinanceApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
99
+ ],
100
+ };
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('es');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('fr');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('id');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('it');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('ja');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('ko');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('nl');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('pl');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('pt');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('ru');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('sv');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('tr');
@@ -0,0 +1,2 @@
1
+ import { createInsuranceDeductibleContent } from './content-factory';
2
+ export const content = createInsuranceDeductibleContent('zh');
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { insuranceDeductibleBreakEvenCalculator } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const INSURANCE_DEDUCTIBLE_BREAK_EVEN_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: insuranceDeductibleBreakEvenCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,424 @@
1
+ :root {
2
+ --idb-ink: #25251f;
3
+ --idb-muted: #716c60;
4
+ --idb-paper: #fbf8ef;
5
+ --idb-surface: #f0eadb;
6
+ --idb-edge: #d6c9aa;
7
+ --idb-claim: #36747b;
8
+ --idb-claim-soft: #d6e8e4;
9
+ --idb-pay: #b5533b;
10
+ --idb-pay-soft: #efd8c8;
11
+ --idb-gold: #bf8a37;
12
+ --idb-on-accent: #fffaf1;
13
+ --idb-shadow: #51463122;
14
+ }
15
+
16
+ :global(.theme-dark) {
17
+ --idb-ink: #f3eee3;
18
+ --idb-muted: #bbb3a4;
19
+ --idb-paper: #24251f;
20
+ --idb-surface: #2d3028;
21
+ --idb-edge: #565446;
22
+ --idb-claim: #84c3c3;
23
+ --idb-claim-soft: #294b4b;
24
+ --idb-pay: #df8663;
25
+ --idb-pay-soft: #5b352b;
26
+ --idb-gold: #e1b45d;
27
+ --idb-on-accent: #281d18;
28
+ --idb-shadow: #0006;
29
+ }
30
+
31
+ .idb-tool {
32
+ background: var(--idb-paper);
33
+ border: 1px solid var(--idb-edge);
34
+ border-radius: 1.35rem;
35
+ box-shadow: 0 1rem 2.5rem var(--idb-shadow);
36
+ color: var(--idb-ink);
37
+ margin: 0 auto;
38
+ max-width: 74rem;
39
+ overflow: hidden;
40
+ width: min(100%, calc(100vw - 2rem));
41
+ }
42
+
43
+ .idb-card {
44
+ display: grid;
45
+ gap: 1.35rem;
46
+ padding: 1.35rem;
47
+ }
48
+
49
+ .idb-glance {
50
+ align-items: stretch;
51
+ background: var(--idb-surface);
52
+ border: 1px solid var(--idb-edge);
53
+ border-radius: 1rem;
54
+ display: grid;
55
+ gap: 1px;
56
+ grid-template-columns: repeat(2, 1fr);
57
+ overflow: hidden;
58
+ }
59
+
60
+ .idb-glance > div {
61
+ display: grid;
62
+ gap: 0.35rem;
63
+ padding: 1rem;
64
+ }
65
+
66
+ .idb-glance > div + div {
67
+ border-left: 1px solid var(--idb-edge);
68
+ }
69
+
70
+ .idb-glance span,
71
+ .idb-result-grid span,
72
+ .idb-visual-heading,
73
+ .idb-help,
74
+ .idb-method,
75
+ .idb-privacy,
76
+ .idb-result-note {
77
+ color: var(--idb-muted);
78
+ font-size: 0.78rem;
79
+ line-height: 1.45;
80
+ }
81
+
82
+ .idb-glance strong {
83
+ font-size: clamp(1.15rem, 2.4vw, 1.65rem);
84
+ line-height: 1.1;
85
+ }
86
+
87
+ .idb-inputs {
88
+ display: grid;
89
+ gap: 1rem;
90
+ grid-template-columns: repeat(2, minmax(0, 1fr));
91
+ }
92
+
93
+ .idb-field {
94
+ display: grid;
95
+ gap: 0.38rem;
96
+ }
97
+
98
+ .idb-field label,
99
+ .idb-label {
100
+ font-size: 0.82rem;
101
+ font-weight: 800;
102
+ }
103
+
104
+ .idb-input-wrap {
105
+ align-items: center;
106
+ background: var(--idb-paper);
107
+ border: 1px solid var(--idb-edge);
108
+ border-radius: 0.65rem;
109
+ display: flex;
110
+ min-height: 2.8rem;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .idb-input-wrap input {
115
+ background: transparent;
116
+ border: 0;
117
+ box-sizing: border-box;
118
+ color: var(--idb-ink);
119
+ font: inherit;
120
+ min-width: 0;
121
+ outline: 0;
122
+ padding: 0.62rem 0.7rem;
123
+ width: 100%;
124
+ }
125
+
126
+ .idb-input-wrap span {
127
+ color: var(--idb-muted);
128
+ font-size: 0.85rem;
129
+ padding: 0 0.7rem;
130
+ }
131
+
132
+ .idb-field > small:not(.idb-help) {
133
+ color: var(--idb-pay);
134
+ font-size: 0.74rem;
135
+ }
136
+
137
+ .idb-field > small[hidden] {
138
+ display: none;
139
+ }
140
+
141
+ .idb-input-wrap:focus-within,
142
+ .idb-currency:focus-visible,
143
+ .idb-button:focus-visible,
144
+ .idb-method summary:focus-visible {
145
+ outline: 3px solid var(--idb-gold);
146
+ outline-offset: 2px;
147
+ }
148
+
149
+ .idb-currency-field,
150
+ .idb-currency-select {
151
+ position: relative;
152
+ }
153
+
154
+ .idb-currency-trigger {
155
+ align-items: center;
156
+ background: var(--idb-paper);
157
+ border: 1px solid var(--idb-edge);
158
+ border-radius: 0.65rem;
159
+ color: var(--idb-ink);
160
+ cursor: pointer;
161
+ display: flex;
162
+ font: inherit;
163
+ justify-content: space-between;
164
+ min-height: 2.8rem;
165
+ padding: 0.62rem 0.7rem;
166
+ text-align: left;
167
+ width: 100%;
168
+ }
169
+
170
+ .idb-currency-trigger-main {
171
+ align-items: center;
172
+ display: flex;
173
+ gap: 0.55rem;
174
+ }
175
+
176
+ .idb-currency-trigger-main strong {
177
+ font-size: 1.1rem;
178
+ line-height: 1;
179
+ }
180
+
181
+ .idb-currency-arrow {
182
+ border-bottom: 2px solid var(--idb-claim);
183
+ border-right: 2px solid var(--idb-claim);
184
+ height: 0.45rem;
185
+ transform: rotate(45deg) translateY(-0.15rem);
186
+ transition: transform 160ms ease;
187
+ width: 0.45rem;
188
+ }
189
+
190
+ .idb-currency-trigger[aria-expanded="true"] .idb-currency-arrow {
191
+ transform: rotate(225deg) translateY(-0.05rem);
192
+ }
193
+
194
+ .idb-currency-menu {
195
+ background: var(--idb-paper);
196
+ border: 1px solid var(--idb-edge);
197
+ border-radius: 0.75rem;
198
+ box-shadow: 0 1rem 2rem var(--idb-shadow);
199
+ display: grid;
200
+ gap: 0.35rem;
201
+ grid-template-columns: repeat(5, minmax(0, 1fr));
202
+ left: 0;
203
+ padding: 0.5rem;
204
+ position: absolute;
205
+ right: 0;
206
+ top: calc(100% + 0.4rem);
207
+ z-index: 20;
208
+ }
209
+
210
+ .idb-currency-menu[hidden] {
211
+ display: none;
212
+ }
213
+
214
+ .idb-currency-option {
215
+ align-items: center;
216
+ background: transparent;
217
+ border: 1px solid transparent;
218
+ border-radius: 0.55rem;
219
+ color: var(--idb-ink);
220
+ cursor: pointer;
221
+ display: flex;
222
+ font: inherit;
223
+ gap: 0.35rem;
224
+ justify-content: center;
225
+ min-width: 0;
226
+ padding: 0.55rem 0.3rem;
227
+ }
228
+
229
+ .idb-currency-option span {
230
+ color: var(--idb-muted);
231
+ font-size: 0.7rem;
232
+ font-weight: 800;
233
+ }
234
+
235
+ .idb-currency-option[aria-selected="true"],
236
+ .idb-currency-option:hover {
237
+ background: var(--idb-claim-soft);
238
+ border-color: var(--idb-claim);
239
+ color: var(--idb-claim);
240
+ }
241
+
242
+ .idb-help {
243
+ margin: 0;
244
+ }
245
+
246
+ .idb-actions {
247
+ align-items: center;
248
+ display: flex;
249
+ flex-wrap: wrap;
250
+ gap: 0.7rem;
251
+ }
252
+
253
+ .idb-button {
254
+ border: 1px solid var(--idb-claim);
255
+ border-radius: 99rem;
256
+ cursor: pointer;
257
+ font: inherit;
258
+ font-weight: 800;
259
+ min-height: 2.75rem;
260
+ padding: 0.55rem 1rem;
261
+ }
262
+
263
+ .idb-button-primary {
264
+ background: var(--idb-claim);
265
+ color: var(--idb-on-accent);
266
+ }
267
+
268
+ .idb-button-secondary {
269
+ background: transparent;
270
+ color: var(--idb-claim);
271
+ }
272
+
273
+ .idb-actions [role="status"] {
274
+ color: var(--idb-muted);
275
+ font-size: 0.78rem;
276
+ }
277
+
278
+ .idb-output {
279
+ border-top: 1px solid var(--idb-edge);
280
+ display: grid;
281
+ gap: 1rem;
282
+ padding-top: 1.35rem;
283
+ }
284
+
285
+ .idb-visual {
286
+ background: var(--idb-surface);
287
+ border: 1px solid var(--idb-edge);
288
+ border-radius: 1rem;
289
+ display: grid;
290
+ gap: 0.8rem;
291
+ padding: 1rem;
292
+ }
293
+
294
+ .idb-visual-heading {
295
+ display: flex;
296
+ font-weight: 800;
297
+ justify-content: space-between;
298
+ }
299
+
300
+ .idb-bars {
301
+ align-items: end;
302
+ display: grid;
303
+ gap: 0.75rem;
304
+ grid-template-columns: 1fr auto 1fr;
305
+ }
306
+
307
+ .idb-bar-side {
308
+ display: grid;
309
+ gap: 0.4rem;
310
+ }
311
+
312
+ .idb-bar-side:first-child {
313
+ text-align: left;
314
+ }
315
+
316
+ .idb-bar-side:last-child {
317
+ text-align: right;
318
+ }
319
+
320
+ .idb-bar-track {
321
+ background: var(--idb-paper);
322
+ border: 1px solid var(--idb-edge);
323
+ border-radius: 99rem;
324
+ height: 1rem;
325
+ overflow: hidden;
326
+ }
327
+
328
+ .idb-bar-track i {
329
+ border-radius: inherit;
330
+ display: block;
331
+ height: 100%;
332
+ min-width: 8%;
333
+ transition: width 180ms ease;
334
+ }
335
+
336
+ .idb-bar-side:first-child i {
337
+ background: var(--idb-claim);
338
+ }
339
+
340
+ .idb-bar-side:last-child i {
341
+ background: var(--idb-pay);
342
+ margin-left: auto;
343
+ }
344
+
345
+ .idb-scale {
346
+ color: var(--idb-gold);
347
+ font-size: 1.55rem;
348
+ font-weight: 800;
349
+ }
350
+
351
+ .idb-result-grid {
352
+ display: grid;
353
+ gap: 0.65rem;
354
+ grid-template-columns: repeat(2, minmax(0, 1fr));
355
+ }
356
+
357
+ .idb-result-grid > div {
358
+ background: var(--idb-surface);
359
+ border: 1px solid var(--idb-edge);
360
+ border-radius: 0.75rem;
361
+ display: grid;
362
+ gap: 0.35rem;
363
+ padding: 0.8rem;
364
+ }
365
+
366
+ .idb-result-grid strong {
367
+ font-size: 1.08rem;
368
+ }
369
+
370
+ .idb-result-grid .idb-emphasis {
371
+ background: var(--idb-claim-soft);
372
+ border-color: var(--idb-claim);
373
+ }
374
+
375
+ .idb-result-note {
376
+ margin: 0;
377
+ }
378
+
379
+ .idb-error-summary {
380
+ background: var(--idb-pay-soft);
381
+ border: 1px solid var(--idb-pay);
382
+ border-radius: 0.65rem;
383
+ color: var(--idb-ink);
384
+ margin: 0;
385
+ padding: 0.7rem 0.8rem;
386
+ }
387
+
388
+ .idb-error-summary[hidden] {
389
+ display: none;
390
+ }
391
+
392
+ .idb-privacy,
393
+ .idb-method {
394
+ margin: 0;
395
+ }
396
+
397
+ .idb-method summary {
398
+ cursor: pointer;
399
+ font-weight: 800;
400
+ }
401
+
402
+ .idb-method p {
403
+ margin: 0.65rem 0 0;
404
+ }
405
+
406
+ @media (max-width: 42rem) {
407
+ .idb-card {
408
+ padding: 1rem;
409
+ }
410
+
411
+ .idb-inputs,
412
+ .idb-result-grid {
413
+ grid-template-columns: 1fr;
414
+ }
415
+
416
+ .idb-glance {
417
+ grid-template-columns: 1fr;
418
+ }
419
+
420
+ .idb-glance > div + div {
421
+ border-left: 0;
422
+ border-top: 1px solid var(--idb-edge);
423
+ }
424
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateInsuranceDeductibleBreakEven, validateInsuranceInput, type InsuranceDeductibleInput } from './logic';
3
+
4
+ const baseInput: InsuranceDeductibleInput = { currency: 'USD', damageCost: 2500, deductible: 500, annualPremium: 1200, premiumIncreasePercent: 10, surchargeYears: 3 };
5
+
6
+ describe('calculateInsuranceDeductibleBreakEven', () => {
7
+ it('compares a claim with paying the full damage cost', () => {
8
+ const result = calculateInsuranceDeductibleBreakEven(baseInput);
9
+ expect(result).toMatchObject({ claimCost: 860, payYourselfCost: 2500, premiumSurcharge: 360, insurerShare: 2000, recommendation: 'claim', breakEvenDamage: 860 });
10
+ });
11
+
12
+ it('recommends paying yourself when the damage is below break even', () => {
13
+ const result = calculateInsuranceDeductibleBreakEven({ ...baseInput, damageCost: 700 });
14
+ expect(result?.recommendation).toBe('payYourself');
15
+ expect(result?.claimCost).toBe(860);
16
+ });
17
+
18
+ it('recognizes the exact break-even amount', () => {
19
+ const result = calculateInsuranceDeductibleBreakEven({ ...baseInput, damageCost: 860 });
20
+ expect(result?.recommendation).toBe('breakEven');
21
+ expect(result?.difference).toBe(0);
22
+ });
23
+
24
+ it('rejects invalid ranges and impossible values', () => {
25
+ const invalid = { ...baseInput, damageCost: 0, premiumIncreasePercent: 101, surchargeYears: 21 };
26
+ expect(Object.keys(validateInsuranceInput(invalid))).toEqual(['damageCost', 'premiumIncreasePercent', 'surchargeYears']);
27
+ expect(calculateInsuranceDeductibleBreakEven(invalid)).toBeNull();
28
+ });
29
+
30
+ it('handles a deductible larger than the damage', () => {
31
+ const result = calculateInsuranceDeductibleBreakEven({ ...baseInput, damageCost: 300, deductible: 500, premiumIncreasePercent: 0 });
32
+ expect(result).toMatchObject({ insurerShare: 0, claimCost: 300, recommendation: 'breakEven' });
33
+ });
34
+
35
+ it('accepts the documented upper bounds without losing a finite result', () => {
36
+ const result = calculateInsuranceDeductibleBreakEven({ currency: 'USD', damageCost: 1_000_000_000_000, deductible: 1_000_000_000_000, annualPremium: 1_000_000_000_000, premiumIncreasePercent: 100, surchargeYears: 20 });
37
+ expect(result?.claimCost).toBe(21_000_000_000_000);
38
+ expect(result?.payYourselfCost).toBe(1_000_000_000_000);
39
+ });
40
+ });