Rueckkaufswerte aller Versicherungen in einem Zug anfragen
Vor Vermoegensverzeichnis und Jahresbericht braucht es von jeder Lebens- und Rentenversicherung den Rueckkaufswert zum Stichtag - jedes Mal dieselben fuenf bis zehn Briefe. Neu unter Vertraege: Stichtag waehlen, Liste pruefen, ein Klick. Heraus kommt ein ZIP mit einer Word-Datei je Gesellschaft plus Begleitblatt; je Vertrag entstehen nebenbei die PDF-Fassung in der Akte, die Wiedervorlage nach 28 Tagen und der Anfragevermerk. Aufbau: - Auswahllogik als reine Funktionen in vertraege/serienabfrage.ts: Vorauswahl, Hindernisse mit Begruendung statt stummem Ausblenden, 28-Tage-Sperre im Gleichschritt mit der Wiedervorlage. - Der Lauf in vertraege/serienlauf.ts: je Vertrag das volle Paket aus Issue #4, aber in einem Durchgang. Kein Alles-oder-nichts - ein gescheiterter Vertrag steht mit Grund im Begleitblatt, die uebrigen laufen durch. Eine Transaktion ueber alle waere die falsche Zusage. - Dublettenschutz tagesgenau: Der Doppelklick erzeugt nichts Doppeltes, die bewusste zweite Anfrage nach einer Woche bleibt moeglich. - Die Anzahl steht vor dem Klick auf dem Knopf. Gekuendigte Vertraege bleiben anfragbar: Bis zum Wirksamwerden besteht der Vertrag, und genau dann will der Sozialhilfetraeger den Wert sehen. Geprueft: zwoelf Einheitstests, drei End-to-End-Tests inklusive der Nebenwirkungen in der Datenbank und des Dublettenschutzes. 602 Unit-Tests, 87 e2e, null Typfehler. Refs #6
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import type { VertragArt, VertragStatus } from '@prisma/client'
|
||||
import { kannRueckkaufswertHaben } from '@/lib/vertraege/konstanten'
|
||||
import { RUECKKAUFSWERT_WIEDERVORLAGE_TAGE } from '@/lib/vertraege/rueckkaufswert'
|
||||
import { heuteAlsTag, tageZwischen } from '@/lib/fristen/datum'
|
||||
|
||||
/**
|
||||
* Die Serienabfrage der Rueckkaufswerte - Auswahllogik.
|
||||
*
|
||||
* Rein und ohne Datenbank: Welche Vertraege kommen in Frage, welche sind
|
||||
* vorausgewaehlt, welche gesperrt? Das sind die Entscheidungen, die
|
||||
* hinterher niemand mehr nachvollziehen kann, wenn sie in einer
|
||||
* Datenbankabfrage stecken. Hier stehen sie einzeln und mit Begruendung.
|
||||
*
|
||||
* Der Anlass: Vor dem Vermoegensverzeichnis und vor jedem Jahresbericht
|
||||
* braucht es von jeder Lebens- und Rentenversicherung den Rueckkaufswert
|
||||
* zum Stichtag. Das sind jedes Mal dieselben fuenf bis zehn Briefe.
|
||||
*/
|
||||
|
||||
export type SerienKandidat = {
|
||||
id: string
|
||||
bezeichnung: string
|
||||
gesellschaft: string | null
|
||||
art: VertragArt
|
||||
status: VertragStatus
|
||||
rueckkaufswertAngefragtAm: Date | null
|
||||
}
|
||||
|
||||
export type KandidatBewertung = {
|
||||
vertrag: SerienKandidat
|
||||
/** Kommt der Vertrag ueberhaupt in Frage? */
|
||||
moeglich: boolean
|
||||
/** Warum nicht? Steht in der Liste, statt den Vertrag stumm auszublenden. */
|
||||
hindernis: string | null
|
||||
/** Vorausgewaehlt? Nur moegliche Vertraege ohne frische Anfrage. */
|
||||
vorausgewaehlt: boolean
|
||||
/** Tage seit der letzten Anfrage, falls eine lief. */
|
||||
tageSeitAnfrage: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sperrfrist: Wer in den letzten 28 Tagen schon gefragt hat, fragt nicht
|
||||
* gleich noch einmal - die Antwort ist schlicht noch unterwegs. Dieselbe
|
||||
* Zahl wie die Wiedervorlage; ein Test haelt beide zusammen.
|
||||
*/
|
||||
export const SPERRFRIST_TAGE = RUECKKAUFSWERT_WIEDERVORLAGE_TAGE
|
||||
|
||||
/**
|
||||
* Bewertet einen Vertrag fuer die Serienabfrage.
|
||||
*
|
||||
* Grundsatz: Nicht stumm ausblenden, sondern begruenden. Ein Vertrag, der
|
||||
* fehlt, wirft die Frage auf "und wo ist der?"; ein Vertrag mit dem
|
||||
* Vermerk "gekuendigt" beantwortet sie gleich mit.
|
||||
*/
|
||||
export function bewerten(vertrag: SerienKandidat, heute: Date = heuteAlsTag()): KandidatBewertung {
|
||||
const tageSeitAnfrage = vertrag.rueckkaufswertAngefragtAm
|
||||
? tageZwischen(vertrag.rueckkaufswertAngefragtAm, heute)
|
||||
: null
|
||||
|
||||
if (!kannRueckkaufswertHaben(vertrag.art)) {
|
||||
return {
|
||||
vertrag,
|
||||
moeglich: false,
|
||||
hindernis: 'Diese Vertragsart bildet keinen Rückkaufswert.',
|
||||
vorausgewaehlt: false,
|
||||
tageSeitAnfrage,
|
||||
}
|
||||
}
|
||||
|
||||
// Verwertet oder abgelaufen: Da gibt es nichts mehr anzufragen. GEKUENDIGT
|
||||
// bleibt dagegen moeglich - bis zum Wirksamwerden der Kuendigung besteht
|
||||
// der Vertrag, und genau dann will der Sozialhilfetraeger den Wert sehen.
|
||||
if (vertrag.status === 'VERWERTET' || vertrag.status === 'ABGELAUFEN') {
|
||||
return {
|
||||
vertrag,
|
||||
moeglich: false,
|
||||
hindernis:
|
||||
vertrag.status === 'VERWERTET'
|
||||
? 'Der Vertrag ist bereits verwertet.'
|
||||
: 'Der Vertrag ist abgelaufen.',
|
||||
vorausgewaehlt: false,
|
||||
tageSeitAnfrage,
|
||||
}
|
||||
}
|
||||
|
||||
if (!vertrag.gesellschaft?.trim()) {
|
||||
return {
|
||||
vertrag,
|
||||
moeglich: false,
|
||||
hindernis: 'Ohne Gesellschaft fehlt der Empfänger — bitte am Vertrag nachtragen.',
|
||||
vorausgewaehlt: false,
|
||||
tageSeitAnfrage,
|
||||
}
|
||||
}
|
||||
|
||||
// Frisch angefragt: moeglich, aber nicht vorausgewaehlt. Wer trotzdem
|
||||
// will (etwa weil der Brief verloren ging), waehlt von Hand an.
|
||||
if (tageSeitAnfrage !== null && tageSeitAnfrage < SPERRFRIST_TAGE) {
|
||||
return {
|
||||
vertrag,
|
||||
moeglich: true,
|
||||
hindernis: `Vor ${tageSeitAnfrage} Tag${tageSeitAnfrage === 1 ? '' : 'en'} bereits angefragt — die Antwort ist vermutlich noch unterwegs.`,
|
||||
vorausgewaehlt: false,
|
||||
tageSeitAnfrage,
|
||||
}
|
||||
}
|
||||
|
||||
return { vertrag, moeglich: true, hindernis: null, vorausgewaehlt: true, tageSeitAnfrage }
|
||||
}
|
||||
|
||||
export function kandidatenBewerten(
|
||||
vertraege: SerienKandidat[],
|
||||
heute: Date = heuteAlsTag(),
|
||||
): KandidatBewertung[] {
|
||||
return vertraege
|
||||
.map((vertrag) => bewerten(vertrag, heute))
|
||||
.sort((a, b) => {
|
||||
// Vorausgewaehlte zuerst, dann die moeglichen, dann der Rest -
|
||||
// jeweils alphabetisch. Was zu tun ist, steht oben.
|
||||
const rang = (k: KandidatBewertung) => (k.vorausgewaehlt ? 0 : k.moeglich ? 1 : 2)
|
||||
if (rang(a) !== rang(b)) return rang(a) - rang(b)
|
||||
return a.vertrag.bezeichnung.localeCompare(b.vertrag.bezeichnung, 'de')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wurde heute schon angefragt? Der Dublettenschutz fuer den zweiten Lauf.
|
||||
*
|
||||
* Bewusst tagesgenau und nicht "innerhalb der Sperrfrist": Wer denselben
|
||||
* Lauf versehentlich zweimal startet, soll keine doppelten Dokumente
|
||||
* bekommen. Wer nach einer Woche bewusst erneut anfragt (Brief verloren),
|
||||
* soll es duerfen.
|
||||
*/
|
||||
export function heuteSchonAngefragt(
|
||||
angefragtAm: Date | null,
|
||||
heute: Date = heuteAlsTag(),
|
||||
): boolean {
|
||||
if (!angefragtAm) return false
|
||||
return tageZwischen(angefragtAm, heute) === 0
|
||||
}
|
||||
Reference in New Issue
Block a user