import { describe, expect, it } from 'vitest' import { tag } from '@/lib/fristen/datum' import { absendetagZu, kuendigungsampel, kuendigungsplan, KUENDIGUNG_VORWARNUNG_TAGE, laufzeitAusVertrag, POSTLAUF_PUFFER_TAGE, } from '@/lib/vertraege/kuendigung' import { kuendigungsassistent, genehmigungsfrage } from '@/lib/vertraege/kuendigungsassistent' import { anfrageStatus, rueckkaufswertAnschreiben, wiedervorlageAm, } from '@/lib/vertraege/rueckkaufswert' import { jahresbeitragCent, kannRueckkaufswertHaben } from '@/lib/vertraege/konstanten' const kopf = { betreuerName: 'Florian Wilde', betreuerStrasse: 'Musterweg 1', betreuerPlz: '95444', betreuerOrt: 'Bayreuth', betreuteName: 'Erika Wilde', betreuteGeburtsdatum: tag(1948, 5, 20), betreuungsgericht: 'Amtsgericht Bayreuth', aktenzeichenGericht: 'XVII 123/25', bestellungAm: tag(2025, 11, 3), } describe('Kündigungstermin', () => { it('rechnet den nächstmöglichen Termin über die Jahresgrenze', () => { // Jahresvertrag seit dem 15.03.2020, drei Monate Kündigungsfrist. // Für den Termin 15.03.2026 müsste der Zugang am 15.12.2025 vorliegen - // der Absendetag ist Anfang Februar 2026 längst vorbei. const plan = kuendigungsplan({ beginnAm: tag(2020, 3, 15), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2026, 2, 1), }) expect(plan.berechenbar).toBe(true) expect(plan.naechsterTermin).toEqual(tag(2027, 3, 15)) expect(plan.zugangSpaetestensAm).toEqual(tag(2026, 12, 15)) expect(plan.spaetesterAbsendetag).toEqual(tag(2026, 12, 12)) expect(plan.verpasst).toBe(false) }) it('rechnet über die Monatsgrenze kalendarisch, nicht in Tagen', () => { // 31.05. minus drei Monate ist der 28.02. (2026 ist kein Schaltjahr), // nicht der 02.03. oder 03.03. const plan = kuendigungsplan({ beginnAm: tag(2019, 5, 31), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2026, 1, 5), }) expect(plan.naechsterTermin).toEqual(tag(2026, 5, 31)) expect(plan.zugangSpaetestensAm).toEqual(tag(2026, 2, 28)) expect(plan.spaetesterAbsendetag).toEqual(tag(2026, 2, 25)) }) it('trifft im Schaltjahr den 29. Februar', () => { const plan = kuendigungsplan({ beginnAm: tag(2019, 5, 31), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2024, 1, 5), }) expect(plan.zugangSpaetestensAm).toEqual(tag(2024, 2, 29)) }) it('springt bei mehrjähriger Laufzeit um die volle Laufzeit weiter', () => { const plan = kuendigungsplan({ beginnAm: tag(2018, 1, 1), laufzeitJahre: 3, kuendigungsfristMonate: 3, heute: tag(2026, 2, 1), }) // Termine: 2021, 2024, 2027 - für 2027 muss bis 30.09.2026 zugehen. expect(plan.naechsterTermin).toEqual(tag(2027, 1, 1)) expect(plan.zugangSpaetestensAm).toEqual(tag(2026, 10, 1)) }) it('rechnet den Postlauf zwischen Absenden und Zugang ein', () => { expect(absendetagZu(tag(2026, 6, 30), 3)).toEqual(tag(2026, 3, 27)) expect(POSTLAUF_PUFFER_TAGE).toBe(3) }) it('warnt, wenn der Absendetag näher als 30 Tage rückt', () => { const plan = kuendigungsplan({ beginnAm: tag(2020, 6, 30), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2026, 3, 20), }) expect(plan.spaetesterAbsendetag).toEqual(tag(2026, 3, 27)) expect(plan.tageBisAbsendetag).toBe(7) expect(plan.vorwarnung).toBe(true) expect(kuendigungsampel(plan)?.variante).toBe('rot') }) it('warnt genau an der Grenze von 30 Tagen', () => { const plan = kuendigungsplan({ beginnAm: tag(2020, 6, 30), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2026, 2, 25), }) expect(plan.tageBisAbsendetag).toBe(KUENDIGUNG_VORWARNUNG_TAGE) expect(plan.vorwarnung).toBe(true) expect(kuendigungsampel(plan)?.variante).toBe('orange') }) it('warnt einen Tag vor der Grenze noch nicht', () => { const plan = kuendigungsplan({ beginnAm: tag(2020, 6, 30), laufzeitJahre: 1, kuendigungsfristMonate: 3, heute: tag(2026, 2, 24), }) expect(plan.tageBisAbsendetag).toBe(31) expect(plan.vorwarnung).toBe(false) }) it('nimmt ohne hinterlegte Laufzeit ein Jahr an und sagt das dazu', () => { const plan = kuendigungsplan({ beginnAm: tag(2020, 3, 15), laufzeitJahre: null, kuendigungsfristMonate: 3, heute: tag(2026, 2, 1), }) expect(plan.naechsterTermin).toEqual(tag(2027, 3, 15)) expect(plan.hinweise.some((h) => h.includes('keine Laufzeit'))).toBe(true) }) it('meldet einen verpassten Termin, wenn sich der Vertrag nicht verlängert', () => { const plan = kuendigungsplan({ beginnAm: tag(2010, 1, 1), endeAm: tag(2026, 3, 1), kuendigungsfristMonate: 3, verlaengertSichAutomatisch: false, heute: tag(2026, 2, 1), }) expect(plan.verpasst).toBe(true) expect(kuendigungsampel(plan)?.variante).toBe('rot') }) it('rechnet gar nicht, wenn weder Beginn noch Ende bekannt sind', () => { const plan = kuendigungsplan({ beginnAm: null, kuendigungsfristMonate: 3, heute: tag(2026, 2, 1), }) expect(plan.berechenbar).toBe(false) expect(plan.naechsterTermin).toBeNull() expect(kuendigungsampel(plan)).toBeNull() }) it('weist auf die fehlende Kündigungsfrist hin, statt still mit null zu rechnen', () => { const plan = kuendigungsplan({ beginnAm: tag(2020, 3, 15), kuendigungsfristMonate: null, heute: tag(2026, 2, 1), }) expect(plan.hinweise.some((h) => h.includes('keine Kündigungsfrist'))).toBe(true) }) }) describe('Rückkaufswert-Anforderung', () => { it('legt die Wiedervorlage vier Wochen nach der Anfrage', () => { expect(wiedervorlageAm(tag(2026, 3, 2))).toEqual(tag(2026, 3, 30)) }) it('fragt Rückkaufswert und beitragsfreie Versicherungssumme zusammen ab', () => { const brief = rueckkaufswertAnschreiben({ kopf, empfaenger: { gesellschaft: 'Musterversicherung AG', anschrift: 'Postfach 1\n80000 München' }, vertragsbezeichnung: 'Sterbegeldversicherung', vertragsnummer: 'LV-4711', stichtag: tag(2026, 1, 1), heute: tag(2026, 3, 2), }) expect(brief).toContain('LV-4711') expect(brief).toContain('Rückkaufswert') expect(brief).toContain('beitragsfreie Versicherungssumme') expect(brief).toContain('Amtsgericht Bayreuth') expect(brief).toContain('XVII 123/25') expect(brief).toContain('01.01.2026') }) it('meldet die Erinnerung, sobald vier Wochen ohne Antwort vergangen sind', () => { expect(anfrageStatus(tag(2026, 3, 2), tag(2026, 3, 29)).erinnerungFaellig).toBe(false) expect(anfrageStatus(tag(2026, 3, 2), tag(2026, 3, 30)).erinnerungFaellig).toBe(true) expect(anfrageStatus(null, tag(2026, 3, 30)).offen).toBe(false) }) }) describe('Kündigungs-Assistent', () => { const eingabe = { kopf, empfaenger: { gesellschaft: 'Musterversicherung AG', anschrift: null }, vertragsbezeichnung: 'Kapitallebensversicherung', vertragsnummer: 'LV-4711', art: 'LEBENSVERSICHERUNG' as const, laufzeit: { beginnAm: tag(2000, 4, 1), laufzeitJahre: 1, kuendigungsfristMonate: 3, }, heute: tag(2026, 2, 1), } it('erzeugt ein Schreiben mit Vertragsnummer und Bestellungsnachweis', () => { const paket = kuendigungsassistent(eingabe) expect(paket.schreiben).toContain('LV-4711') expect(paket.schreiben).toContain('kündige ich den oben genannten Vertrag') expect(paket.schreiben).toContain('rechtlichen Betreuer') expect(paket.schreiben).toContain('hilfsweise zum nächstmöglichen Zeitpunkt') }) it('stellt bei einer Lebensversicherung die Genehmigungsfrage, ohne sie zu beantworten', () => { const paket = kuendigungsassistent(eingabe) expect(paket.genehmigung.stellen).toBe(true) expect(paket.genehmigung.rechtsgrundlage).toBe('§ 1850 BGB') expect(paket.genehmigung.frage).toMatch(/Rechtspfleger/) expect(paket.genehmigung.hinweis).toMatch(/entscheidet das ausdrücklich nicht/) }) it('stellt bei einer Hausratversicherung keine Genehmigungsfrage', () => { expect(genehmigungsfrage('HAUSRAT').stellen).toBe(false) expect(genehmigungsfrage('LEBENSVERSICHERUNG').stellen).toBe(true) }) it('verlangt immer einen Versandnachweis', () => { const paket = kuendigungsassistent(eingabe) expect(paket.versandnachweis.verlangt).toBe(true) expect(paket.versandnachweis.moeglichkeiten.join(' ')).toMatch(/Einschreiben/) expect(paket.versandnachweis.moeglichkeiten.join(' ')).toMatch(/Fax/) }) it('erinnert vor der Kündigung an die Beitragsfreistellung als Alternative', () => { const paket = kuendigungsassistent(eingabe) expect(paket.warnungen.some((w) => w.includes('beitragsfreie Versicherungssumme'))).toBe(true) }) it('warnt bei einer Bestattungsvorsorge vor der vorschnellen Kündigung', () => { const paket = kuendigungsassistent({ ...eingabe, art: 'BESTATTUNGSVORSORGE' }) expect(paket.warnungen.some((w) => w.includes('§ 90 Abs. 3 SGB XII'))).toBe(true) }) it('warnt, wenn die Vertragsnummer fehlt', () => { const paket = kuendigungsassistent({ ...eingabe, vertragsnummer: null }) expect(paket.warnungen.some((w) => w.includes('Vertragsnummer fehlt'))).toBe(true) }) }) describe('Beiträge und Vertragsarten', () => { it('rechnet den Jahresbeitrag aus Beitrag und Rhythmus', () => { expect(jahresbeitragCent(2_500n, 'MONATLICH')).toBe(30_000n) expect(jahresbeitragCent(2_500n, 'VIERTELJAEHRLICH')).toBe(10_000n) expect(jahresbeitragCent(2_500n, 'JAEHRLICH')).toBe(2_500n) expect(jahresbeitragCent(2_500n, 'EINMALIG')).toBeNull() expect(jahresbeitragCent(null, 'MONATLICH')).toBeNull() }) it('kennt die Verträge, bei denen ein Rückkaufswert überhaupt anfällt', () => { expect(kannRueckkaufswertHaben('LEBENSVERSICHERUNG')).toBe(true) expect(kannRueckkaufswertHaben('STERBEGELDVERSICHERUNG')).toBe(true) expect(kannRueckkaufswertHaben('STROM')).toBe(false) }) }) describe('Vertragsdauer aus Beginn und Ende', () => { it('leitet die Laufzeit aus einem festen Vertragsende ab', () => { expect(laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1))).toBe(3) expect(laufzeitAusVertrag(tag(2020, 3, 15), tag(2021, 3, 15))).toBe(1) }) it('gibt null zurück, wenn ein Datum fehlt oder die Spanne unter einem Jahr liegt', () => { expect(laufzeitAusVertrag(tag(2020, 1, 1), null)).toBeNull() expect(laufzeitAusVertrag(null, tag(2021, 1, 1))).toBeNull() expect(laufzeitAusVertrag(tag(2020, 1, 1), tag(2020, 7, 1))).toBeNull() }) it('lässt einen dreijährigen Vertrag um drei Jahre weiterspringen', () => { const plan = kuendigungsplan({ beginnAm: tag(2018, 1, 1), endeAm: tag(2021, 1, 1), laufzeitJahre: laufzeitAusVertrag(tag(2018, 1, 1), tag(2021, 1, 1)), kuendigungsfristMonate: 3, heute: tag(2026, 2, 1), }) expect(plan.naechsterTermin).toEqual(tag(2027, 1, 1)) }) })