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
140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
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
|
|
}
|