Ausschliesslich Zeilenenden. Nachgewiesen mit "git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich kein Zeichen Inhalt geaendert. Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit echten Aenderungen zu verstecken, macht die Historie an dieser Stelle unlesbar. So laesst er sich beim Durchsehen ueberspringen. Refs #26
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
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,
|
|
juengsterWert,
|
|
rueckkaufswertAnschreiben,
|
|
wertHistorie,
|
|
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))
|
|
})
|
|
})
|
|
|
|
describe('Rückkaufswert-Historie am Vertrag', () => {
|
|
const wert = (jahr: number, cent: number) => ({ stichtag: tag(jahr, 3, 1), wertCent: cent })
|
|
|
|
it('sortiert über alle verknüpften Posten hinweg, jüngster zuerst', () => {
|
|
// Zwei Posten am selben Vertrag: einer von Hand angelegt, einer beim
|
|
// ersten Erfassen entstanden. Der Vertrag zeigt beide Reihen als eine.
|
|
const historie = wertHistorie([
|
|
{ wertstaende: [wert(2024, 4000)] },
|
|
{ wertstaende: [wert(2026, 5200), wert(2025, 4600)] },
|
|
])
|
|
|
|
expect(historie.map((w) => w.wertCent)).toEqual([5200, 4600, 4000])
|
|
})
|
|
|
|
it('nennt den zuletzt mitgeteilten Wert', () => {
|
|
expect(juengsterWert([{ wertstaende: [wert(2025, 4600), wert(2026, 5200)] }])?.wertCent).toBe(
|
|
5200,
|
|
)
|
|
})
|
|
|
|
it('gibt ohne jeden Wert null zurück, statt eine Null zu behaupten', () => {
|
|
expect(juengsterWert([{ wertstaende: [] }])).toBeNull()
|
|
expect(juengsterWert([])).toBeNull()
|
|
})
|
|
})
|