import { alsIsoTag, heuteAlsTag, plusJahre, plusMonate, plusTage, tageZwischen, tagesdatum } from '@/lib/fristen/datum' /** * Kuendigungsfrist-Rechner. * * Die eigentliche Falle bei Versicherungsvertraegen ist nicht der * Kuendigungstermin, sondern der Tag, an dem der Brief spaetestens raus * muss. Eine Kuendigung wird mit dem ZUGANG beim Versicherer wirksam, nicht * mit dem Absenden - wer am letzten Tag der Frist einwirft, hat die Frist * verpasst und zahlt ein weiteres Jahr. * * Deshalb rechnet diese Datei drei Daten aus: * * 1. den naechstmoeglichen Kuendigungstermin (zu wann kann gekuendigt werden), * 2. den Tag, an dem die Kuendigung spaetestens zugehen muss, * 3. den spaetesten Absendetag mit Postlaufpuffer. * * Reine Logik, keine Datenbank. */ /** * Puffer zwischen Absenden und Zugang. Drei Kalendertage sind knapp, aber * bei einem Einwurf-Einschreiben realistisch. Wer es genauer braucht, * schickt zusaetzlich ein Fax oder eine E-Mail - beides zaehlt fuer den * Zugang und ist am selben Tag da. */ export const POSTLAUF_PUFFER_TAGE = 3 /** Ab wann wird gewarnt, dass der Absendetag naeher rueckt. */ export const KUENDIGUNG_VORWARNUNG_TAGE = 30 /** * Verlaengerung, wenn im Vertrag keine Laufzeit hinterlegt ist. Ein Jahr * ist bei Versicherungen der Regelfall (§ 11 Abs. 1 VVG). */ export const STANDARD_LAUFZEIT_JAHRE = 1 export type KuendigungEingabe = { /** Laufzeitbeginn laut Vertrag. */ beginnAm: Date | null /** Fest vereinbartes Vertragsende, falls bekannt. */ endeAm?: Date | null /** Vertragsdauer in Jahren; fehlt sie, wird mit einem Jahr gerechnet. */ laufzeitJahre?: number | null /** Kuendigungsfrist in Monaten zum Ende der Laufzeit. */ kuendigungsfristMonate: number | null /** Verlaengert sich der Vertrag stillschweigend? Regelfall: ja. */ verlaengertSichAutomatisch?: boolean /** Bezugstag; im Test gesetzt, sonst heute. */ heute?: Date } export type Kuendigungsplan = { /** Laesst sich ueberhaupt etwas berechnen? */ berechenbar: boolean /** Zu diesem Tag kann als Naechstes gekuendigt werden. */ naechsterTermin: Date | null /** Bis zu diesem Tag muss die Kuendigung beim Vertragspartner sein. */ zugangSpaetestensAm: Date | null /** Bis zu diesem Tag muss der Brief raus (Zugang minus Postlauf). */ spaetesterAbsendetag: Date | null /** Tage von heute bis zum Absendetag. Negativ heisst: schon vorbei. */ tageBisAbsendetag: number | null /** Weniger als 30 Tage bis zum Absendetag - jetzt handeln. */ vorwarnung: boolean /** Der Absendetag ist verstrichen und der Vertrag laeuft aus. */ verpasst: boolean /** Erklaerungen und Annahmen, die in die Rechnung eingegangen sind. */ hinweise: string[] } const LEERER_PLAN: Kuendigungsplan = { berechenbar: false, naechsterTermin: null, zugangSpaetestensAm: null, spaetesterAbsendetag: null, tageBisAbsendetag: null, vorwarnung: false, verpasst: false, hinweise: [], } /** * Berechnet den naechstmoeglichen Kuendigungstermin. * * Vorgehen: Vom ersten Ablauftermin an wird geprueft, ob der zugehoerige * Absendetag noch in der Zukunft liegt. Ist er verstrichen, wird um eine * Laufzeit weitergerueckt. Der erste Termin, dessen Brief noch rechtzeitig * rausgeht, ist der naechstmoegliche. */ export function kuendigungsplan(eingabe: KuendigungEingabe): Kuendigungsplan { const heute = tagesdatum(eingabe.heute ?? heuteAlsTag()) const hinweise: string[] = [] const frist = eingabe.kuendigungsfristMonate if (frist === null || frist === undefined) { hinweise.push( 'Es ist keine Kündigungsfrist hinterlegt. Es wird ohne Frist gerechnet — ' + 'bitte im Vertrag nachsehen und nachtragen.', ) } const fristMonate = frist ?? 0 const laufzeit = eingabe.laufzeitJahre && eingabe.laufzeitJahre > 0 ? eingabe.laufzeitJahre : STANDARD_LAUFZEIT_JAHRE if (!eingabe.laufzeitJahre || eingabe.laufzeitJahre <= 0) { hinweise.push( `Im Vertrag steht keine Laufzeit. Gerechnet wird mit einer Verlängerung um ` + `${STANDARD_LAUFZEIT_JAHRE} Jahr — bei Versicherungen der Regelfall.`, ) } const verlaengert = eingabe.verlaengertSichAutomatisch ?? true // Erster Ablauftermin: das vereinbarte Ende, sonst Beginn plus Laufzeit. let termin: Date | null = null if (eingabe.endeAm) { termin = tagesdatum(eingabe.endeAm) } else if (eingabe.beginnAm) { termin = plusJahre(tagesdatum(eingabe.beginnAm), laufzeit) } if (!termin) { return { ...LEERER_PLAN, hinweise: [ 'Ohne Laufzeitbeginn oder Vertragsende lässt sich kein Kündigungstermin ' + 'berechnen. Bitte mindestens den Vertragsbeginn eintragen.', ], } } // Bis zu 200 Verlaengerungen durchgehen - deckt auch einen Vertrag von // 1962 ab, ohne bei fehlerhaften Daten endlos zu laufen. let absendetag = absendetagZu(termin, fristMonate) let durchlaeufe = 0 while (absendetag.getTime() < heute.getTime() && verlaengert && durchlaeufe < 200) { termin = plusJahre(termin, laufzeit) absendetag = absendetagZu(termin, fristMonate) durchlaeufe += 1 } const zugang = plusMonate(termin, -fristMonate) const tageBis = tageZwischen(heute, absendetag) const verpasst = absendetag.getTime() < heute.getTime() if (verpasst) { hinweise.push( `Der Absendetag für den Termin ${alsIsoTag(termin)} ist verstrichen. ` + `Dieser Vertrag verlängert sich nicht automatisch — eine Kündigung zu diesem ` + `Termin ist nicht mehr möglich.`, ) } else if (durchlaeufe > 0) { hinweise.push( `Der Vertrag hat sich bereits ${durchlaeufe}-mal verlängert. Angezeigt ist der ` + `nächste Termin, zu dem noch rechtzeitig gekündigt werden kann.`, ) } hinweise.push( `Eine Kündigung wird mit dem Zugang wirksam, nicht mit dem Absenden. Deshalb ` + `sind zwischen Absendetag und Zugang ${POSTLAUF_PUFFER_TAGE} Tage Postlauf ` + `eingerechnet. Am sichersten: Einschreiben mit Rückschein oder zusätzlich ` + `ein Fax mit Sendeprotokoll.`, ) return { berechenbar: true, naechsterTermin: termin, zugangSpaetestensAm: zugang, spaetesterAbsendetag: absendetag, tageBisAbsendetag: tageBis, vorwarnung: !verpasst && tageBis <= KUENDIGUNG_VORWARNUNG_TAGE, verpasst, hinweise, } } /** Der Absendetag zu einem Termin: Termin minus Frist minus Postlauf. */ export function absendetagZu(termin: Date, fristMonate: number): Date { return plusTage(plusMonate(tagesdatum(termin), -fristMonate), -POSTLAUF_PUFFER_TAGE) } /** * Kurzfassung fuer die Uebersichtstabelle: eine Ampelstufe je Vertrag. * Farbe ist nie die einzige Information - der Text steht daneben. */ export type Kuendigungsampel = { variante: 'rot' | 'orange' | 'gelb' | 'gruen' text: string } export function kuendigungsampel(plan: Kuendigungsplan): Kuendigungsampel | null { if (!plan.berechenbar || plan.tageBisAbsendetag === null) return null if (plan.verpasst) { return { variante: 'rot', text: 'Absendetag verstrichen' } } const tage = plan.tageBisAbsendetag if (tage <= 7) return { variante: 'rot', text: `Brief muss in ${tage} Tagen raus` } if (tage <= KUENDIGUNG_VORWARNUNG_TAGE) { return { variante: 'orange', text: `Brief muss in ${tage} Tagen raus` } } if (tage <= 60) return { variante: 'gelb', text: `Absendetag in ${tage} Tagen` } return { variante: 'gruen', text: `Absendetag in ${tage} Tagen` } } /** * Die Vertragsdauer aus Beginn und vereinbartem Ende. * * Das Datenmodell kennt kein Feld "Laufzeit" (siehe * docs/schema-wuensche/substanz.md, Punkt 8). Stehen aber beide Daten im * Vertrag, ergibt sich die Dauer daraus: Ein Vertrag vom 01.01.2018 bis * 01.01.2021 hat eine dreijaehrige Laufzeit und verlaengert sich um * dieselbe Spanne. Gerundet wird auf volle Jahre - ein Jahr sind 365 Tage, * ein Schaltjahr 366, und beides soll dieselbe Laufzeit ergeben. Liegt die * Spanne unter einem halben Jahr, bleibt es beim Standard. */ export function laufzeitAusVertrag( beginnAm: Date | null, endeAm: Date | null, ): number | null { if (!beginnAm || !endeAm) return null const jahre = Math.round(tageZwischen(beginnAm, endeAm) / 365.25) return jahre >= 1 ? jahre : null }