feat: Immobilie, Versicherungen und Heim mit COPD-Verlauf
Etappe 6. Das leerstehende Haus, die Vertraege und der Gesundheitsverlauf - die drei Bereiche, in denen fuer diesen Fall das meiste Geld und das meiste Risiko liegt. Leerstands-Checkliste mit zehn Punkten, jeder mit Begruendung. Der wichtigste: den Gebaeudeversicherer ueber den Leerstand informieren - sonst droht Leistungsfreiheit, und zwar genau dann, wenn es brennt oder eine Leitung platzt. Dazu Verkehrssicherungspflicht, Frostschutz zum Oktober, Kontrollgaenge, Zaehlerstaende, Schluesselverzeichnis. Der Kuendigungsrechner nennt den spaetesten ABSENDETAG, nicht den Fristablauf: eine Kuendigung wirkt mit dem Zugang, nicht mit dem Abschicken. Drei Tage Postlaufpuffer sind einkalkuliert. Der Kuendigungs-Assistent beantwortet die Genehmigungsfrage nach § 1850 BGB nicht - er stellt sie und verlangt den Gang zum Rechtspfleger. Der Versandnachweis ist Pflichtfeld: ohne ihn kommt die Kuendigung nicht durch die Pruefung. Leistungszuschlag nach § 43c SGB XI mit Stand 01.01.2024 (15/30/50/75 Prozent nach Verweildauer, erst ab Pflegegrad 2). Die Oberflaeche zeigt, ab wann die naechste Stufe greift - das ist bares Geld und wird regelmaessig uebersehen. COPD-Schnellerfassung fuer Exazerbation, Sauerstoff, Sturz, Krankenhaus, Hilfsmittel und Impfungen, mit STIKO-Plan fuer COPD. Beschreibungen verschluesselt. Die Jahresauswertung ueber Exazerbationen und Krankenhaustage wandert in den Jahresbericht und begruendet eine Hoeherstufung. 74 Tests. Etappe: 6 Phase: Substanz
This commit is contained in:
+171
-172
@@ -1,172 +1,171 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
optionalerBetrag,
|
||||
optionalerText,
|
||||
optionaleZahl,
|
||||
optionalesDatum,
|
||||
pflichtBetrag,
|
||||
pflichtDatum,
|
||||
pflichtText,
|
||||
} from '@/lib/validierung/substanz'
|
||||
import { WERTQUELLEN_VERTRAG } from '@/lib/vertraege/konstanten'
|
||||
|
||||
/**
|
||||
* Eingabepruefung fuer Versicherungen und Vertraege.
|
||||
*/
|
||||
|
||||
export const VERTRAG_ARTEN = [
|
||||
'LEBENSVERSICHERUNG',
|
||||
'STERBEGELDVERSICHERUNG',
|
||||
'BESTATTUNGSVORSORGE',
|
||||
'RENTENVERSICHERUNG_PRIVAT',
|
||||
'UNFALLVERSICHERUNG',
|
||||
'HAFTPFLICHT',
|
||||
'HAUSRAT',
|
||||
'WOHNGEBAEUDE',
|
||||
'KRANKENZUSATZ',
|
||||
'PFLEGEZUSATZ',
|
||||
'RECHTSSCHUTZ',
|
||||
'STERBEKASSE',
|
||||
'STROM',
|
||||
'GAS',
|
||||
'WASSER',
|
||||
'TELEFON_INTERNET',
|
||||
'RUNDFUNKBEITRAG',
|
||||
'MITGLIEDSCHAFT',
|
||||
'ABONNEMENT',
|
||||
'SONSTIGES',
|
||||
] as const
|
||||
|
||||
export const VERTRAG_STATUS = [
|
||||
'AKTIV',
|
||||
'BEITRAGSFREI',
|
||||
'RUHEND',
|
||||
'GEKUENDIGT',
|
||||
'ABGETRETEN',
|
||||
'VERWERTET',
|
||||
'ABGELAUFEN',
|
||||
] as const
|
||||
|
||||
export const ZAHL_RHYTHMEN = [
|
||||
'MONATLICH',
|
||||
'VIERTELJAEHRLICH',
|
||||
'HALBJAEHRLICH',
|
||||
'JAEHRLICH',
|
||||
'EINMALIG',
|
||||
] as const
|
||||
|
||||
export const BEWERTUNGEN = ['UNGEPRUEFT', 'VERWERTBAR', 'GESCHUETZT', 'STRITTIG'] as const
|
||||
|
||||
export const VertragSchema = z
|
||||
.object({
|
||||
bezeichnung: pflichtText(
|
||||
150,
|
||||
'Bitte eine Bezeichnung angeben, z. B. „Sterbegeldversicherung Allianz“.',
|
||||
),
|
||||
gesellschaft: optionalerText(150),
|
||||
art: z.enum(VERTRAG_ARTEN),
|
||||
status: z.enum(VERTRAG_STATUS).default('AKTIV'),
|
||||
|
||||
/** Wird verschlüsselt gespeichert. */
|
||||
vertragsnummer: optionalerText(80),
|
||||
|
||||
beginnAm: optionalesDatum,
|
||||
endeAm: optionalesDatum,
|
||||
kuendigungsfristMonate: optionaleZahl(
|
||||
0,
|
||||
120,
|
||||
'Bitte die Kündigungsfrist in ganzen Monaten angeben.',
|
||||
),
|
||||
laufzeitJahre: optionaleZahl(1, 99, 'Bitte die Laufzeit in ganzen Jahren angeben.'),
|
||||
kuendigungZum: optionalesDatum,
|
||||
gekuendigtAm: optionalesDatum,
|
||||
|
||||
beitrag: optionalerBetrag,
|
||||
rhythmus: z.enum(ZAHL_RHYTHMEN).nullable().catch(null),
|
||||
|
||||
bewertungSozialhilfe: z.enum(BEWERTUNGEN).default('UNGEPRUEFT'),
|
||||
bewertungBegruendung: optionalerText(4000),
|
||||
|
||||
notizen: optionalerText(4000),
|
||||
})
|
||||
.superRefine((daten, kontext) => {
|
||||
if (daten.beginnAm && daten.endeAm && daten.endeAm.getTime() < daten.beginnAm.getTime()) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['endeAm'],
|
||||
message: 'Das Vertragsende liegt vor dem Beginn.',
|
||||
})
|
||||
}
|
||||
if (daten.beitrag !== null && !daten.rhythmus) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['rhythmus'],
|
||||
message: 'Zu einem Beitrag gehört der Zahlungsrhythmus — sonst lässt sich der Jahresbeitrag nicht rechnen.',
|
||||
})
|
||||
}
|
||||
if (daten.bewertungSozialhilfe !== 'UNGEPRUEFT' && !daten.bewertungBegruendung) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['bewertungBegruendung'],
|
||||
message:
|
||||
'Bitte begründen, warum du den Vertrag so einordnest. Der Bezirk fragt danach, ' +
|
||||
'und im Zweifel steht diese Begründung Jahre später im Widerspruchsverfahren.',
|
||||
})
|
||||
}
|
||||
if (daten.status === 'GEKUENDIGT' && !daten.gekuendigtAm) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['gekuendigtAm'],
|
||||
message: 'Bitte das Datum der Kündigung angeben.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type VertragEingabe = z.infer<typeof VertragSchema>
|
||||
|
||||
/** Rückkaufswert zu einem Stichtag - Historie, nichts wird überschrieben. */
|
||||
export const RueckkaufswertSchema = z.object({
|
||||
stichtag: pflichtDatum,
|
||||
wert: pflichtBetrag,
|
||||
wertquelle: z.enum(WERTQUELLEN_VERTRAG, {
|
||||
errorMap: () => ({ message: 'Bitte angeben, woher der Wert stammt.' }),
|
||||
}),
|
||||
bemerkung: optionalerText(1000),
|
||||
})
|
||||
|
||||
export type RueckkaufswertEingabe = z.infer<typeof RueckkaufswertSchema>
|
||||
|
||||
export const RueckkaufswertAnfrageSchema = z.object({
|
||||
stichtag: pflichtDatum,
|
||||
empfaengerAnschrift: optionalerText(500),
|
||||
})
|
||||
|
||||
export type RueckkaufswertAnfrageEingabe = z.infer<typeof RueckkaufswertAnfrageSchema>
|
||||
|
||||
/**
|
||||
* Kündigung vermerken. Der Versandnachweis ist Pflicht: Ohne ihn lässt
|
||||
* sich der Zugang nicht beweisen, und beweisen muss ihn derjenige, der
|
||||
* gekündigt hat.
|
||||
*/
|
||||
export const KuendigungVermerkenSchema = z
|
||||
.object({
|
||||
gekuendigtAm: pflichtDatum,
|
||||
kuendigungZum: optionalesDatum,
|
||||
versandart: pflichtText(100, 'Bitte angeben, wie die Kündigung versandt wurde.'),
|
||||
nachweisVorhanden: z.boolean().default(false),
|
||||
genehmigungGeklaert: z.boolean().default(false),
|
||||
bemerkung: optionalerText(2000),
|
||||
})
|
||||
.superRefine((daten, kontext) => {
|
||||
if (!daten.nachweisVorhanden) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['nachweisVorhanden'],
|
||||
message:
|
||||
'Ohne Versandnachweis wird die Kündigung nicht als erledigt geführt. ' +
|
||||
'Lade Einschreibenbeleg oder Faxprotokoll als Dokument hoch und hake dann an.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type KuendigungVermerkenEingabe = z.infer<typeof KuendigungVermerkenSchema>
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
optionalerBetrag,
|
||||
optionalerText,
|
||||
optionaleZahl,
|
||||
optionalesDatum,
|
||||
pflichtBetrag,
|
||||
pflichtDatum,
|
||||
pflichtText,
|
||||
} from '@/lib/validierung/substanz'
|
||||
import { WERTQUELLEN_VERTRAG } from '@/lib/vertraege/konstanten'
|
||||
|
||||
/**
|
||||
* Eingabepruefung fuer Versicherungen und Vertraege.
|
||||
*/
|
||||
|
||||
export const VERTRAG_ARTEN = [
|
||||
'LEBENSVERSICHERUNG',
|
||||
'STERBEGELDVERSICHERUNG',
|
||||
'BESTATTUNGSVORSORGE',
|
||||
'RENTENVERSICHERUNG_PRIVAT',
|
||||
'UNFALLVERSICHERUNG',
|
||||
'HAFTPFLICHT',
|
||||
'HAUSRAT',
|
||||
'WOHNGEBAEUDE',
|
||||
'KRANKENZUSATZ',
|
||||
'PFLEGEZUSATZ',
|
||||
'RECHTSSCHUTZ',
|
||||
'STERBEKASSE',
|
||||
'STROM',
|
||||
'GAS',
|
||||
'WASSER',
|
||||
'TELEFON_INTERNET',
|
||||
'RUNDFUNKBEITRAG',
|
||||
'MITGLIEDSCHAFT',
|
||||
'ABONNEMENT',
|
||||
'SONSTIGES',
|
||||
] as const
|
||||
|
||||
export const VERTRAG_STATUS = [
|
||||
'AKTIV',
|
||||
'BEITRAGSFREI',
|
||||
'RUHEND',
|
||||
'GEKUENDIGT',
|
||||
'ABGETRETEN',
|
||||
'VERWERTET',
|
||||
'ABGELAUFEN',
|
||||
] as const
|
||||
|
||||
export const ZAHL_RHYTHMEN = [
|
||||
'MONATLICH',
|
||||
'VIERTELJAEHRLICH',
|
||||
'HALBJAEHRLICH',
|
||||
'JAEHRLICH',
|
||||
'EINMALIG',
|
||||
] as const
|
||||
|
||||
export const BEWERTUNGEN = ['UNGEPRUEFT', 'VERWERTBAR', 'GESCHUETZT', 'STRITTIG'] as const
|
||||
|
||||
export const VertragSchema = z
|
||||
.object({
|
||||
bezeichnung: pflichtText(
|
||||
150,
|
||||
'Bitte eine Bezeichnung angeben, z. B. „Sterbegeldversicherung Allianz“.',
|
||||
),
|
||||
gesellschaft: optionalerText(150),
|
||||
art: z.enum(VERTRAG_ARTEN),
|
||||
status: z.enum(VERTRAG_STATUS).default('AKTIV'),
|
||||
|
||||
/** Wird verschlüsselt gespeichert. */
|
||||
vertragsnummer: optionalerText(80),
|
||||
|
||||
beginnAm: optionalesDatum,
|
||||
endeAm: optionalesDatum,
|
||||
kuendigungsfristMonate: optionaleZahl(
|
||||
0,
|
||||
120,
|
||||
'Bitte die Kündigungsfrist in ganzen Monaten angeben.',
|
||||
),
|
||||
kuendigungZum: optionalesDatum,
|
||||
gekuendigtAm: optionalesDatum,
|
||||
|
||||
beitrag: optionalerBetrag,
|
||||
rhythmus: z.enum(ZAHL_RHYTHMEN).nullable().catch(null),
|
||||
|
||||
bewertungSozialhilfe: z.enum(BEWERTUNGEN).default('UNGEPRUEFT'),
|
||||
bewertungBegruendung: optionalerText(4000),
|
||||
|
||||
notizen: optionalerText(4000),
|
||||
})
|
||||
.superRefine((daten, kontext) => {
|
||||
if (daten.beginnAm && daten.endeAm && daten.endeAm.getTime() < daten.beginnAm.getTime()) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['endeAm'],
|
||||
message: 'Das Vertragsende liegt vor dem Beginn.',
|
||||
})
|
||||
}
|
||||
if (daten.beitrag !== null && !daten.rhythmus) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['rhythmus'],
|
||||
message: 'Zu einem Beitrag gehört der Zahlungsrhythmus — sonst lässt sich der Jahresbeitrag nicht rechnen.',
|
||||
})
|
||||
}
|
||||
if (daten.bewertungSozialhilfe !== 'UNGEPRUEFT' && !daten.bewertungBegruendung) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['bewertungBegruendung'],
|
||||
message:
|
||||
'Bitte begründen, warum du den Vertrag so einordnest. Der Bezirk fragt danach, ' +
|
||||
'und im Zweifel steht diese Begründung Jahre später im Widerspruchsverfahren.',
|
||||
})
|
||||
}
|
||||
if (daten.status === 'GEKUENDIGT' && !daten.gekuendigtAm) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['gekuendigtAm'],
|
||||
message: 'Bitte das Datum der Kündigung angeben.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type VertragEingabe = z.infer<typeof VertragSchema>
|
||||
|
||||
/** Rückkaufswert zu einem Stichtag - Historie, nichts wird überschrieben. */
|
||||
export const RueckkaufswertSchema = z.object({
|
||||
stichtag: pflichtDatum,
|
||||
wert: pflichtBetrag,
|
||||
wertquelle: z.enum(WERTQUELLEN_VERTRAG, {
|
||||
errorMap: () => ({ message: 'Bitte angeben, woher der Wert stammt.' }),
|
||||
}),
|
||||
bemerkung: optionalerText(1000),
|
||||
})
|
||||
|
||||
export type RueckkaufswertEingabe = z.infer<typeof RueckkaufswertSchema>
|
||||
|
||||
export const RueckkaufswertAnfrageSchema = z.object({
|
||||
stichtag: pflichtDatum,
|
||||
empfaengerAnschrift: optionalerText(500),
|
||||
})
|
||||
|
||||
export type RueckkaufswertAnfrageEingabe = z.infer<typeof RueckkaufswertAnfrageSchema>
|
||||
|
||||
/**
|
||||
* Kündigung vermerken. Der Versandnachweis ist Pflicht: Ohne ihn lässt
|
||||
* sich der Zugang nicht beweisen, und beweisen muss ihn derjenige, der
|
||||
* gekündigt hat.
|
||||
*/
|
||||
export const KuendigungVermerkenSchema = z
|
||||
.object({
|
||||
gekuendigtAm: pflichtDatum,
|
||||
kuendigungZum: optionalesDatum,
|
||||
versandart: pflichtText(100, 'Bitte angeben, wie die Kündigung versandt wurde.'),
|
||||
nachweisVorhanden: z.boolean().default(false),
|
||||
genehmigungGeklaert: z.boolean().default(false),
|
||||
bemerkung: optionalerText(2000),
|
||||
})
|
||||
.superRefine((daten, kontext) => {
|
||||
if (!daten.nachweisVorhanden) {
|
||||
kontext.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['nachweisVorhanden'],
|
||||
message:
|
||||
'Ohne Versandnachweis wird die Kündigung nicht als erledigt geführt. ' +
|
||||
'Lade Einschreibenbeleg oder Faxprotokoll als Dokument hoch und hake dann an.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type KuendigungVermerkenEingabe = z.infer<typeof KuendigungVermerkenSchema>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Briefkopf } from '@/lib/vertraege/anschreiben'
|
||||
|
||||
/**
|
||||
* Baut den Briefkopf aus Stammdaten und angemeldetem Benutzer.
|
||||
*
|
||||
* Die Anschrift des Betreuers steht im Datenmodell nicht - der Benutzer
|
||||
* hat nur Name und E-Mail. Sie bleibt deshalb leer; im erzeugten Text
|
||||
* fehlen dann schlicht die beiden Zeilen, und wer den Entwurf kopiert,
|
||||
* ergaenzt sie im Briefkopf seines Textverarbeitungsprogramms ohnehin.
|
||||
*/
|
||||
export type BriefkopfQuelle = {
|
||||
betreute: {
|
||||
vorname: string
|
||||
nachname: string
|
||||
anrede: string | null
|
||||
geburtsdatum: Date
|
||||
heimName: string | null
|
||||
heimStrasse: string | null
|
||||
heimPlz: string | null
|
||||
heimOrt: string | null
|
||||
betreuungsgericht: string | null
|
||||
aktenzeichenGericht: string | null
|
||||
bestellungAm: Date | null
|
||||
}
|
||||
benutzer: { name: string }
|
||||
}
|
||||
|
||||
export function briefkopfBauen(quelle: BriefkopfQuelle): Briefkopf {
|
||||
const b = quelle.betreute
|
||||
const anschrift = [
|
||||
b.heimName,
|
||||
b.heimStrasse,
|
||||
[b.heimPlz, b.heimOrt].filter(Boolean).join(' ') || null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
return {
|
||||
betreuerName: quelle.benutzer.name,
|
||||
betreuerStrasse: null,
|
||||
betreuerPlz: null,
|
||||
// Der Ort in der Datumszeile ist der Ort des Betreuers, nicht der des
|
||||
// Heims - er steht nirgends im Datenmodell und bleibt deshalb leer.
|
||||
betreuerOrt: null,
|
||||
betreuteName: `${b.anrede ? `${b.anrede} ` : ''}${b.vorname} ${b.nachname}`.trim(),
|
||||
betreuteGeburtsdatum: b.geburtsdatum,
|
||||
betreuteAnschrift: anschrift || null,
|
||||
betreuungsgericht: b.betreuungsgericht,
|
||||
aktenzeichenGericht: b.aktenzeichenGericht,
|
||||
bestellungAm: b.bestellungAm,
|
||||
}
|
||||
}
|
||||
+227
-207
@@ -1,207 +1,227 @@
|
||||
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` }
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user