@jjlmoya/utils-finance 1.45.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.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/mfe_assets_contract.test.ts +48 -0
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/loanAprScenarioCalculator/bibliography.astro +11 -0
  8. package/src/tool/loanAprScenarioCalculator/bibliography.ts +29 -0
  9. package/src/tool/loanAprScenarioCalculator/component.astro +62 -0
  10. package/src/tool/loanAprScenarioCalculator/controller.ts +205 -0
  11. package/src/tool/loanAprScenarioCalculator/currency-dom.ts +107 -0
  12. package/src/tool/loanAprScenarioCalculator/currency.test.ts +57 -0
  13. package/src/tool/loanAprScenarioCalculator/currency.ts +105 -0
  14. package/src/tool/loanAprScenarioCalculator/dom-views.ts +190 -0
  15. package/src/tool/loanAprScenarioCalculator/entry.ts +28 -0
  16. package/src/tool/loanAprScenarioCalculator/evaluator.ts +21 -0
  17. package/src/tool/loanAprScenarioCalculator/i18n/de.ts +194 -0
  18. package/src/tool/loanAprScenarioCalculator/i18n/en.ts +194 -0
  19. package/src/tool/loanAprScenarioCalculator/i18n/es.ts +195 -0
  20. package/src/tool/loanAprScenarioCalculator/i18n/fr.ts +195 -0
  21. package/src/tool/loanAprScenarioCalculator/i18n/id.ts +193 -0
  22. package/src/tool/loanAprScenarioCalculator/i18n/it.ts +193 -0
  23. package/src/tool/loanAprScenarioCalculator/i18n/ja.ts +186 -0
  24. package/src/tool/loanAprScenarioCalculator/i18n/ko.ts +186 -0
  25. package/src/tool/loanAprScenarioCalculator/i18n/nl.ts +194 -0
  26. package/src/tool/loanAprScenarioCalculator/i18n/pl.ts +194 -0
  27. package/src/tool/loanAprScenarioCalculator/i18n/pt.ts +194 -0
  28. package/src/tool/loanAprScenarioCalculator/i18n/ru.ts +193 -0
  29. package/src/tool/loanAprScenarioCalculator/i18n/sv.ts +189 -0
  30. package/src/tool/loanAprScenarioCalculator/i18n/tr.ts +193 -0
  31. package/src/tool/loanAprScenarioCalculator/i18n/zh.ts +181 -0
  32. package/src/tool/loanAprScenarioCalculator/index.ts +11 -0
  33. package/src/tool/loanAprScenarioCalculator/loan-apr-scenario-calculator.css +699 -0
  34. package/src/tool/loanAprScenarioCalculator/logic.test.ts +65 -0
  35. package/src/tool/loanAprScenarioCalculator/logic.ts +259 -0
  36. package/src/tool/loanAprScenarioCalculator/presets.ts +51 -0
  37. package/src/tool/loanAprScenarioCalculator/scenario-dom.ts +101 -0
  38. package/src/tool/loanAprScenarioCalculator/seo.astro +11 -0
  39. package/src/tool/loanAprScenarioCalculator/storage.ts +60 -0
  40. package/src/tool/loanAprScenarioCalculator/ui.ts +55 -0
  41. package/src/tool/loanAprScenarioCalculator/validation.ts +24 -0
  42. package/src/tool/renterInsuranceCoverageGapCalculator/bibliography.astro +11 -0
  43. package/src/tool/renterInsuranceCoverageGapCalculator/bibliography.ts +6 -0
  44. package/src/tool/renterInsuranceCoverageGapCalculator/component.astro +80 -0
  45. package/src/tool/renterInsuranceCoverageGapCalculator/controller.ts +102 -0
  46. package/src/tool/renterInsuranceCoverageGapCalculator/currency.test.ts +17 -0
  47. package/src/tool/renterInsuranceCoverageGapCalculator/currency.ts +69 -0
  48. package/src/tool/renterInsuranceCoverageGapCalculator/dom-views.ts +68 -0
  49. package/src/tool/renterInsuranceCoverageGapCalculator/entry.ts +28 -0
  50. package/src/tool/renterInsuranceCoverageGapCalculator/evaluator.ts +17 -0
  51. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/de.ts +40 -0
  52. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/en.ts +98 -0
  53. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/es.ts +38 -0
  54. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/fr.ts +38 -0
  55. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/id.ts +38 -0
  56. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/it.ts +38 -0
  57. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/ja.ts +38 -0
  58. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/ko.ts +38 -0
  59. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/nl.ts +38 -0
  60. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/pl.ts +38 -0
  61. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/pt.ts +38 -0
  62. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/ru.ts +38 -0
  63. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/sv.ts +38 -0
  64. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/tr.ts +38 -0
  65. package/src/tool/renterInsuranceCoverageGapCalculator/i18n/zh.ts +38 -0
  66. package/src/tool/renterInsuranceCoverageGapCalculator/index.ts +11 -0
  67. package/src/tool/renterInsuranceCoverageGapCalculator/logic.test.ts +52 -0
  68. package/src/tool/renterInsuranceCoverageGapCalculator/logic.ts +108 -0
  69. package/src/tool/renterInsuranceCoverageGapCalculator/renter-insurance-coverage-gap-calculator.css +462 -0
  70. package/src/tool/renterInsuranceCoverageGapCalculator/seo.astro +11 -0
  71. package/src/tool/renterInsuranceCoverageGapCalculator/storage.ts +23 -0
  72. package/src/tool/renterInsuranceCoverageGapCalculator/ui.ts +48 -0
  73. package/src/tools.ts +3 -1
@@ -0,0 +1,38 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { RenterInsuranceCoverageGapCalculatorUI } from '../ui';
4
+ export const ui: RenterInsuranceCoverageGapCalculatorUI = {
5
+ labelPolicyLimit: 'Eşya teminat limiti', labelDeductible: 'Muafiyet', labelSampleLoss: 'Örnek hasar', labelCurrency: 'Para birimi', labelApproximateRates: 'Dönüşüm, canlı kurlar yerine yaklaşık planlama katsayılarını kullanır.', labelPolicyBoundary: 'Poliçe sınırı', labelSetLimit: 'Sınırı belirle', labelPolicyBoundaryHint: 'Poliçenizdeki eşya tutarını kullanın ve ardından olası bir kaybı deneyin.', labelInventory: 'Yenileme envanteri', labelInventoryHint: 'Her kategoride her şeyi bugün yenilemenin maliyetini tahmin edin.', labelPolicyLimitHelp: 'Bina limitini değil, poliçedeki eşya teminat limitini kullanın.', labelDeductibleHelp: 'Eşya hasarı için geçerli muafiyeti girin.', labelSampleLossHelp: 'Yalnızca açık bulunduğunda örnek bir hasar senaryosu göstermek için kullanılır.', labelTotalContents: 'Tahmini eşya değeri', labelCoverageGap: 'Olası teminat açığı', labelCoverageRatio: 'Tahmini teminat oranı', labelEstimatedPayout: 'Örnek tazminat', labelOutOfPocket: 'Örnek cepten ödeme', labelStatus: 'Teminat değerlendirmesi', labelStatusSafe: 'Limit envanteri karşılıyor', labelStatusReview: 'Küçük açığı incele', labelStatusExposed: 'Eşyalar eksik sigortalı', labelStatusSafeText: 'Girdiğiniz eşya limiti en az yenileme envanteri kadar. Özel eşyalar için ayrı limitleri kontrol edin.', labelStatusReviewText: 'Limit envantere yakın, ancak küçük bir açık var. Pahalı eşyaları ve yeni alışverişleri yeniden kontrol edin.', labelStatusExposedText: 'Limit envanter tahmininin altında. Tam kayıp durumunda poliçeye güvenmeden önce poliçeyi inceleyin.', labelLargestCategories: 'En büyük envanter kategorileri', labelCoverageLine: 'Poliçe limiti', labelReplacementValue: 'Yenileme değeri', labelLimitMarker: 'Limit işareti', labelLossScenario: 'Kayıp senaryosu', labelIllustrativePayout: 'Yalnızca örnektir: oransal tazminat maddesi poliçeniz için geçerli olmayabilir.', labelReset: 'Örneği sıfırla', labelCopy: 'Özeti kopyala', labelCopied: 'Özet kopyalandı', labelCopyFailed: 'Sonucu elle kopyalayın', labelInvalidInput: 'Vurgulanan değeri kontrol edin', labelMethodTitle: 'Bu tahmin nasıl çalışır', labelMethodText: 'Araç kategori tahminlerinizi toplar ve eşya limitiyle karşılaştırır. Açık, bu değerler arasındaki pozitif farktır. Örnek tazminat, teminat oranını kayıp senaryosuna uygular ve muafiyeti çıkarır. Bu bir planlama modelidir, hasar ödemesi tahmini değildir. Poliçenizde eşya alt limitleri, yeni değer kuralları, istisnalar, ilk risk teminatı veya eksik sigorta için farklı bir uygulama bulunabilir.', labelPrivacyNote: 'Envanteriniz bu tarayıcıda kalır. Hesap, yükleme veya sunucu hesabı gerekmez.', labelAmountPlaceholder: '0,00', labelCategoryFurniture: 'Mobilya ve dekorasyon', labelCategoryElectronics: 'Elektronik', labelCategoryClothing: 'Giyim ve ayakkabı', labelCategoryKitchen: 'Mutfak ve cihazlar', labelCategoryPersonal: 'Kişisel eşyalar', labelCategoryOther: 'Diğer eşyalar',
6
+ };
7
+ const slug = 'kiraci-esya-sigortasi-teminat-acigi-hesaplayici';
8
+ const title = 'Kiracı eşya sigortası teminat açığı hesaplayıcısı';
9
+ const description = 'Kiracı sigortanızdaki eşya limitinin eşyalarınızın yenileme değerine ulaşıp ulaşmadığını tahmin edin. Envanteri, poliçe limitini, muafiyeti ve örnek hasarı tarayıcıda karşılaştırın.';
10
+ const faq = [
11
+ { question: 'Bu kiracı sigortası hesaplayıcısı neyi ölçer?', answer: 'Girdiğiniz ev eşyası yenileme değerlerini toplar ve poliçe eşya limitiyle karşılaştırır. Fark, limit dışında kalabilecek tutarın planlama tahminidir.' },
12
+ { question: 'Envanter için hangi değeri kullanmalıyım?', answer: 'Mobilya, elektronik, giyim, cihaz ve kişisel eşyaların güncel ve gerçekçi yenileme maliyetini kullanın. Poliçenizin istediği değerleme temelini kontrol edin.' },
13
+ { question: 'Sonuç eksik sigortayı kanıtlar mı?', answer: 'Hayır. Sonuç envantere, poliçe metnine ve değerleme varsayımlarına bağlıdır. Alt limitler, istisnalar ve eksik sigorta maddeleri hasarı değiştirebilir.' },
14
+ { question: 'Hesaplayıcı neden örnek tazminat gösteriyor?', answer: 'Girdiğiniz kayıp için basit bir oransal modelin muafiyet sonrasındaki sonucunu gösterir. Sigortacının gerçekte ne ödeyeceğini öngörmez.' },
15
+ { question: 'Ev sahibinin sabit eşyalarını eklemeli miyim?', answer: 'Poliçenizde eşya teminatına girenleri ekleyin. Sorumluluk, bina, sabit donanım ve eşyalar ayrı bölümler olabilir.' },
16
+ ];
17
+ const howTo = [
18
+ { name: 'Yenileme envanteri oluşturun', text: 'Her odayı dolaşın ve eşyaları bugün yenilemenin maliyetini girin. En yüksek değerleri görünür tutmak için altı kategori kullanın.' },
19
+ { name: 'Eşya limitini girin', text: 'Poliçe veya teklifteki eşya sigorta bedelini kopyalayın. Bina ya da sorumluluk limitini kullanmayın.' },
20
+ { name: 'Muafiyet ve kaybı ekleyin', text: 'Eşya hasarlarında geçerli muafiyeti ve örnek kayıp tutarını girerek olası cepten ödemeyi görün.' },
21
+ { name: 'Karar sınırını inceleyin', text: 'Açık oluşursa pahalı eşyaları, alt limitleri, değerli eşyaları ve yeni alışverişleri sigortacı veya broker ile kontrol edin.' },
22
+ ];
23
+ export const content: ToolLocaleContent<RenterInsuranceCoverageGapCalculatorUI> = { slug, title, description, ui, seo: [
24
+ { type: 'title', text: 'Limiti eşyalarınızın değeriyle karşılaştırın', level: 2 },
25
+ { type: 'paragraph', html: '<p>Kiralık ev eşyası sigortası, limit oda oda çıkarılan envanter yerine yuvarlak bir tutarla seçildiğinde kolayca yetersiz kalır. Bu hesaplayıcı kendi yenileme tahminlerinizi görünür bir teminat sınırına dönüştürür.</p>' },
26
+ { type: 'title', text: 'Hesaplamadan önce toplanacaklar', level: 2 },
27
+ { type: 'list', items: ['Mobilya, elektronik, giyim, cihaz ve kişisel eşyalar için yenileme tahmini.', 'Bina ve sorumluluktan ayrı poliçe eşya limiti.', 'Eşya hasarında uygulanan muafiyet.', 'Örnek tazminatı incelemek için gerçekçi bir kayıp tutarı.'] },
28
+ { type: 'title', text: 'Teminat açığını nasıl okuyabilirsiniz', level: 2 },
29
+ { type: 'paragraph', html: '<p>Açık, limitin üzerindeki eşya değeridir. Sıfır açık varsayımlarınıza göre iki toplamın buluştuğunu gösterir; her eşyanın teminat altında olduğunu doğrulamaz. Pozitif açık, değerleme temelini, alt limitleri, istisnaları ve eksik sigorta maddesini incelemek için uyarıdır.</p>' },
30
+ { type: 'table', headers: ['İşaret', 'Ne anlatır', 'Sonraki yararlı adım'], rows: [['Limit envanteri karşılıyor', 'Girilen limit en az yenileme toplamına ulaşıyor.', 'Değerli eşyaları, alt limitleri ve istisnaları kontrol edin.'], ['Küçük açığı incele', 'Limit yakın, ancak yeni alışverişler veya yuvarlama önemli olabilir.', 'En büyük kategorileri yeniden kontrol edin ve poliçeyi güncelleyin.'], ['Eşyalar eksik sigortalı', 'Girilen limit yenileme toplamının belirgin biçimde altında.', 'Yeni teklif isteyin veya eksik sigorta uygulamasını netleştirin.']] },
31
+ { type: 'title', text: 'Tahminin sınırları', level: 2 },
32
+ { type: 'paragraph', html: '<p>Bu araç poliçeyi okumaz, hasarın teminat altında olup olmadığına karar vermez ve bağlayıcı bir tazminat hesaplamaz. Senaryo basit bir oransal model kullanır ve muafiyeti çıkarır. Gerçek poliçelerde farklı maddeler, limitler, değerleme kuralları ve istisnalar olabilir.</p>' },
33
+ { type: 'tip', title: 'Belge saklama önerisi', html: '<p>Değerli eşyalar için faturaları, fotoğrafları ve tarihli bir envanteri saklayın. Financial Ombudsman Service, tam yenileme maliyetinin hesaplanmamasının neden eksik sigortaya yol açabildiğini açıklar; İspanya sigorta sözleşmesi kanunu sigorta bedelini azami tazminat olarak tanımlar ve poliçe aksini söylemiyorsa oransal kural öngörür.</p>' },
34
+ ], faq, bibliography, howTo, schemas: [
35
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
36
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) },
37
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'FinanceApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'TRY' } },
38
+ ] };
@@ -0,0 +1,38 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { RenterInsuranceCoverageGapCalculatorUI } from '../ui';
4
+ export const ui: RenterInsuranceCoverageGapCalculatorUI = {
5
+ labelPolicyLimit: '家庭财产保险限额', labelDeductible: '免赔额', labelSampleLoss: '示例损失', labelCurrency: '货币', labelApproximateRates: '换算使用规划用的近似系数,不是实时汇率。', labelPolicyBoundary: '保单边界', labelSetLimit: '设置限额', labelPolicyBoundaryHint: '使用保单中的家庭财产金额,然后测试一次可能的损失。', labelInventory: '重置成本清单', labelInventoryHint: '按类别估算今天重新购置全部物品需要的费用。', labelPolicyLimitHelp: '使用保单中的家庭财产限额,不要使用建筑物限额。', labelDeductibleHelp: '输入家庭财产理赔适用的免赔额。', labelSampleLossHelp: '仅用于在发现缺口后展示示例理赔情景。', labelTotalContents: '估计财产价值', labelCoverageGap: '潜在保障缺口', labelCoverageRatio: '估计保障比例', labelEstimatedPayout: '示例赔付', labelOutOfPocket: '示例自付金额', labelStatus: '保障评估', labelStatusSafe: '限额覆盖清单', labelStatusReview: '检查小额缺口', labelStatusExposed: '财产保障不足', labelStatusSafeText: '输入的财产限额至少达到清单中的重置成本。高价值物品的单项限额仍需单独检查。', labelStatusReviewText: '限额接近清单总额,但仍有小额缺口。请重新检查贵重物品和最近购买的物品。', labelStatusExposedText: '限额低于清单估值。在依靠保单应对全损之前,请先查看具体条款。', labelLargestCategories: '清单中金额最大的类别', labelCoverageLine: '保单限额', labelReplacementValue: '重置价值', labelLimitMarker: '限额标记', labelLossScenario: '损失情景', labelIllustrativePayout: '仅供参考:比例赔付条款不一定适用于您的保单。', labelReset: '重置示例', labelCopy: '复制摘要', labelCopied: '摘要已复制', labelCopyFailed: '请手动复制结果', labelInvalidInput: '检查突出显示的数值', labelMethodTitle: '估算方式', labelMethodText: '工具会汇总各类别估值,并与家庭财产限额比较。缺口是两者之间的正差值。示例赔付将保障比例应用于损失情景,再扣除免赔额。这是规划模型,不是实际理赔金额预测。保单可能包含单项限额、新价值规则、除外责任、第一风险保障或其他不足额保险处理方式。', labelPrivacyNote: '您的清单保留在此浏览器中。不需要账户、上传或服务器计算。', labelAmountPlaceholder: '0.00', labelCategoryFurniture: '家具和装饰', labelCategoryElectronics: '电子产品', labelCategoryClothing: '衣物和鞋类', labelCategoryKitchen: '厨房和家电', labelCategoryPersonal: '个人物品', labelCategoryOther: '其他财产',
6
+ };
7
+ const slug = 'renter-insurance-coverage-gap-calculator';
8
+ const title = '租房者家庭财产保险保障缺口计算器';
9
+ const description = '估算租房者家庭财产保险限额是否达到物品的重置价值。在浏览器中比较清单、保单限额、免赔额和示例损失。';
10
+ const faq = [
11
+ { question: '这个租房保险计算器衡量什么?', answer: '它会汇总您输入的家庭财产重置价值,并与保单限额比较。差额是可能超出保障限额金额的规划估算。' },
12
+ { question: '清单应使用什么价值?', answer: '请使用家具、电子产品、衣物、家电和个人物品当前合理的重置成本,并确认保单要求的新价值或其他估值方式。' },
13
+ { question: '结果能证明保障不足吗?', answer: '不能。结果取决于清单、保单文字和估值假设。单项限额、除外责任、特别约定和不足额保险条款都可能改变理赔结果。' },
14
+ { question: '为什么显示示例赔付?', answer: '它展示简单比例模型对输入损失额计算的结果,并扣除免赔额。这个数字用于理解缺口,不代表保险公司的实际赔付预测。' },
15
+ { question: '需要计入房东的固定设施吗?', answer: '只计入按保单属于您家庭财产保障的物品。责任、建筑物、固定设施和家庭财产可能属于不同保障部分。' },
16
+ ];
17
+ const howTo = [
18
+ { name: '建立重置成本清单', text: '逐个房间查看物品,输入今天重新购置家庭财产的成本。分成六类后,最大金额来源会更清楚。' },
19
+ { name: '输入家庭财产限额', text: '从保单明细或报价单中抄录家庭财产保险金额,不要使用建筑物或责任限额。' },
20
+ { name: '加入免赔额和损失', text: '输入家庭财产理赔的免赔额和示例损失金额,查看示例自付情景。' },
21
+ { name: '检查决策边界', text: '如果出现缺口,请与保险公司或经纪人核对贵重物品、单项限额、价值物品和最近购买的物品。' },
22
+ ];
23
+ export const content: ToolLocaleContent<RenterInsuranceCoverageGapCalculatorUI> = { slug, title, description, ui, seo: [
24
+ { type: 'title', text: '将保险限额与物品价值比较', level: 2 },
25
+ { type: 'paragraph', html: '<p>租房者家庭财产保险在只选一个整数限额、没有按房间建立清单时容易不足。这款计算器将您自己的重置成本估算变成可见的保障边界,帮助您在需要理赔前发现缺口。</p>' },
26
+ { type: 'title', text: '计算前准备什么', level: 2 },
27
+ { type: 'list', items: ['家具、电子产品、衣物、家电和个人物品的重置成本估算。', '保单中的家庭财产限额,并与建筑物和责任保障分开。', '家庭财产理赔适用的免赔额。', '用于探索示例赔付的现实损失金额。'] },
28
+ { type: 'title', text: '如何理解保障缺口', level: 2 },
29
+ { type: 'paragraph', html: '<p>缺口是超过限额的家庭财产价值。缺口为零表示按您的假设两个总额相符,但不能确认每件物品都受保障。正数缺口提示您检查估值方式、高价物品单项限额、除外责任和不足额保险条款。</p>' },
30
+ { type: 'table', headers: ['提示', '含义', '下一步'], rows: [['限额覆盖清单', '输入的限额至少达到重置成本总额。', '检查价值物品、单项限额和除外责任。'], ['检查小额缺口', '限额接近总额,但新购买或四舍五入可能有影响。', '重新查看最大类别并更新保单。'], ['财产保障不足', '输入限额明显低于重置成本总额。', '索取新报价或确认不足额保险的处理方式。']] },
31
+ { type: 'title', text: '估算的局限', level: 2 },
32
+ { type: 'paragraph', html: '<p>此工具不会读取保单,不判断损失是否承保,也不会计算有约束力的理赔金额。示例情景使用简单比例模型并扣除免赔额。真实保单可能包含不同的条款、限额、估值规则、除外责任或第一风险保障。</p>' },
33
+ { type: 'tip', title: '留存证据的建议', html: '<p>为高价值物品保存收据、照片和带日期的清单。Financial Ombudsman Service 解释了没有计算完整重置成本为何会造成保障不足;西班牙保险合同法将保险金额视为最高赔偿额,除非保单另有约定,还规定了比例规则。</p>' },
34
+ ], faq, bibliography, howTo, schemas: [
35
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
36
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) },
37
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'FinanceApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' } },
38
+ ] };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { renterInsuranceCoverageGapCalculator } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const RENTER_INSURANCE_COVERAGE_GAP_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: renterInsuranceCoverageGapCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateRenterInsuranceCoverageGap, validateRenterInsuranceInput, type RenterInsuranceInput } from './logic';
3
+
4
+ const baseInput: RenterInsuranceInput = {
5
+ currency: 'EUR',
6
+ policyLimit: 18000,
7
+ deductible: 250,
8
+ sampleLoss: 5000,
9
+ inventory: [
10
+ { category: 'furniture', value: 7000 },
11
+ { category: 'electronics', value: 6000 },
12
+ { category: 'clothing', value: 2500 },
13
+ { category: 'kitchen', value: 1500 },
14
+ { category: 'personal', value: 1000 },
15
+ { category: 'other', value: 1000 },
16
+ ],
17
+ };
18
+
19
+ describe('calculateRenterInsuranceCoverageGap', () => {
20
+ it('finds the coverage gap and ranks the largest inventory categories', () => {
21
+ const result = calculateRenterInsuranceCoverageGap(baseInput);
22
+ expect(result).toMatchObject({ totalContents: 19000, coverageGap: 1000, coverageRatio: 18000 / 19000, uncoveredPercent: 5.26, status: 'review' });
23
+ expect(result?.largestCategories.map((item) => item.category)).toEqual(['furniture', 'electronics', 'clothing']);
24
+ });
25
+
26
+ it('estimates the illustrative payout after proportional cover and deductible', () => {
27
+ const result = calculateRenterInsuranceCoverageGap(baseInput);
28
+ expect(result).toMatchObject({ estimatedPayout: 4486.84, estimatedOutOfPocket: 513.16 });
29
+ });
30
+
31
+ it('marks a fully covered inventory as safe', () => {
32
+ const result = calculateRenterInsuranceCoverageGap({ ...baseInput, policyLimit: 25000 });
33
+ expect(result).toMatchObject({ coverageGap: 0, coverageRatio: 1, uncoveredPercent: 0, status: 'safe' });
34
+ });
35
+
36
+ it('marks a large gap as exposed', () => {
37
+ const result = calculateRenterInsuranceCoverageGap({ ...baseInput, policyLimit: 10000 });
38
+ expect(result?.status).toBe('exposed');
39
+ expect(result?.coverageGap).toBe(9000);
40
+ });
41
+
42
+ it('rejects empty inventories and impossible policy values', () => {
43
+ const invalid = { ...baseInput, policyLimit: 0, sampleLoss: 0, inventory: [] };
44
+ expect(Object.keys(validateRenterInsuranceInput(invalid))).toEqual(['policyLimit', 'sampleLoss', 'inventory']);
45
+ expect(calculateRenterInsuranceCoverageGap(invalid)).toBeNull();
46
+ });
47
+
48
+ it('does not let a sample loss exceed the estimated contents', () => {
49
+ const result = calculateRenterInsuranceCoverageGap({ ...baseInput, sampleLoss: 50000, policyLimit: 25000, deductible: 0 });
50
+ expect(result).toMatchObject({ estimatedPayout: 19000, estimatedOutOfPocket: 0 });
51
+ });
52
+ });
@@ -0,0 +1,108 @@
1
+ export const INVENTORY_CATEGORIES = ['furniture', 'electronics', 'clothing', 'kitchen', 'personal', 'other'] as const;
2
+
3
+ export type InventoryCategory = (typeof INVENTORY_CATEGORIES)[number];
4
+
5
+ export interface InventoryItem {
6
+ category: InventoryCategory;
7
+ value: number;
8
+ }
9
+
10
+ export interface RenterInsuranceInput {
11
+ currency: string;
12
+ policyLimit: number;
13
+ deductible: number;
14
+ sampleLoss: number;
15
+ inventory: InventoryItem[];
16
+ }
17
+
18
+ export type CoverageStatus = 'safe' | 'review' | 'exposed';
19
+
20
+ export interface RenterInsuranceValidationErrors {
21
+ currency?: string;
22
+ policyLimit?: string;
23
+ deductible?: string;
24
+ sampleLoss?: string;
25
+ inventory?: string;
26
+ }
27
+
28
+ export interface CoverageCategoryResult {
29
+ category: InventoryCategory;
30
+ value: number;
31
+ share: number;
32
+ }
33
+
34
+ export interface RenterInsuranceResult {
35
+ currency: string;
36
+ policyLimit: number;
37
+ deductible: number;
38
+ sampleLoss: number;
39
+ totalContents: number;
40
+ coverageGap: number;
41
+ coverageRatio: number;
42
+ uncoveredPercent: number;
43
+ estimatedPayout: number;
44
+ estimatedOutOfPocket: number;
45
+ largestCategories: CoverageCategoryResult[];
46
+ status: CoverageStatus;
47
+ }
48
+
49
+ const MAX_AMOUNT = 1_000_000_000;
50
+
51
+ const roundMoney = (value: number): number => Number(value.toFixed(2));
52
+ const isAmount = (value: number): boolean => Number.isFinite(value) && value >= 0 && value <= MAX_AMOUNT;
53
+ const isPositiveAmount = (value: number): boolean => isAmount(value) && value > 0;
54
+ const isCurrency = (value: string): boolean => /^[A-Z]{3}$/.test(value.trim().toUpperCase());
55
+ const isValidInventory = (inventory: unknown): inventory is InventoryItem[] => {
56
+ if (!Array.isArray(inventory) || inventory.length !== INVENTORY_CATEGORIES.length) return false;
57
+ return inventory.every((item) => INVENTORY_CATEGORIES.includes(item.category) && isAmount(item.value));
58
+ };
59
+ const getInventoryTotal = (inventory: InventoryItem[]): number => inventory.reduce((sum, item) => sum + item.value, 0);
60
+
61
+ export function validateRenterInsuranceInput(input: RenterInsuranceInput): RenterInsuranceValidationErrors {
62
+ const errors: RenterInsuranceValidationErrors = {};
63
+ if (!isCurrency(input.currency)) errors.currency = 'currency';
64
+ if (!isPositiveAmount(input.policyLimit)) errors.policyLimit = 'policyLimit';
65
+ if (!isAmount(input.deductible)) errors.deductible = 'deductible';
66
+ if (!isPositiveAmount(input.sampleLoss)) errors.sampleLoss = 'sampleLoss';
67
+ if (!isValidInventory(input.inventory)) errors.inventory = 'inventory';
68
+ if (isValidInventory(input.inventory)) {
69
+ const total = getInventoryTotal(input.inventory);
70
+ if (!isPositiveAmount(total)) errors.inventory = 'inventory';
71
+ }
72
+ return errors;
73
+ }
74
+
75
+ const getStatus = (coverageGap: number, totalContents: number): CoverageStatus => {
76
+ if (coverageGap <= 0) return 'safe';
77
+ return coverageGap / totalContents <= 0.1 ? 'review' : 'exposed';
78
+ };
79
+
80
+ const sortCategories = (inventory: InventoryItem[], totalContents: number): CoverageCategoryResult[] => inventory
81
+ .map((item) => ({ category: item.category, value: roundMoney(item.value), share: item.value / totalContents }))
82
+ .sort((left, right) => right.value - left.value)
83
+ .slice(0, 3);
84
+
85
+ export function calculateRenterInsuranceCoverageGap(input: RenterInsuranceInput): RenterInsuranceResult | null {
86
+ if (Object.keys(validateRenterInsuranceInput(input)).length > 0) return null;
87
+ const currency = input.currency.trim().toUpperCase();
88
+ const totalContents = roundMoney(getInventoryTotal(input.inventory));
89
+ const coverageRatio = Math.min(input.policyLimit / totalContents, 1);
90
+ const coverageGap = roundMoney(Math.max(0, totalContents - input.policyLimit));
91
+ const loss = Math.min(input.sampleLoss, totalContents);
92
+ const estimatedPayout = roundMoney(Math.max(0, loss * coverageRatio - input.deductible));
93
+ const estimatedOutOfPocket = roundMoney(Math.max(0, loss - estimatedPayout));
94
+ return {
95
+ currency,
96
+ policyLimit: roundMoney(input.policyLimit),
97
+ deductible: roundMoney(input.deductible),
98
+ sampleLoss: roundMoney(input.sampleLoss),
99
+ totalContents,
100
+ coverageGap,
101
+ coverageRatio,
102
+ uncoveredPercent: roundMoney(Math.max(0, 1 - coverageRatio) * 100),
103
+ estimatedPayout,
104
+ estimatedOutOfPocket,
105
+ largestCategories: sortCategories(input.inventory, totalContents),
106
+ status: getStatus(coverageGap, totalContents),
107
+ };
108
+ }