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:
Florian Wilde
2026-08-23 12:10:32 +02:00
parent 96a9bc4d63
commit 9fe6f10f25
14 changed files with 1345 additions and 250 deletions
+185
View File
@@ -0,0 +1,185 @@
import { expect, test } from '@playwright/test'
import { PrismaClient } from '@prisma/client'
import { anmelden } from './hilfen'
/**
* Serienabfrage der Rückkaufswerte — gegen die echte Anwendung.
*
* Geprüft wird der ganze Lauf: Vorauswahl, ZIP-Download, die
* Nebenwirkungen (Anfragedatum, Wiedervorlage, Ausgangsdokumente) und der
* Dublettenschutz beim zweiten Lauf am selben Tag.
*/
const KENNUNG = 'Prüfhausen Serie'
async function mitDb<T>(arbeit: (db: PrismaClient) => Promise<T>): Promise<T> {
const db = new PrismaClient()
try {
return await arbeit(db)
} finally {
await db.$disconnect()
}
}
async function aufraeumen(): Promise<void> {
await mitDb(async (db) => {
await db.dokument.deleteMany({ where: { empfaenger: { contains: KENNUNG } } })
await db.frist.deleteMany({ where: { titel: { contains: KENNUNG } } })
await db.vertrag.deleteMany({ where: { bezeichnung: { contains: KENNUNG } } })
})
}
test.describe('Serienabfrage', () => {
test.beforeAll(async () => {
await aufraeumen()
await mitDb(async (db) => {
const betreute = await db.betreute.findMany({
where: { geloeschtAm: null },
select: { id: true },
})
for (const person of betreute) {
// Zwei anfragbare, eine ohne Rueckkaufswert, eine ohne Gesellschaft.
await db.vertrag.createMany({
data: [
{
betreuteId: person.id,
bezeichnung: `${KENNUNG} Leben`,
gesellschaft: `${KENNUNG} Lebensversicherung`,
art: 'LEBENSVERSICHERUNG',
status: 'AKTIV',
},
{
betreuteId: person.id,
bezeichnung: `${KENNUNG} Sterbegeld`,
gesellschaft: `${KENNUNG} Sterbekasse`,
art: 'STERBEGELDVERSICHERUNG',
status: 'AKTIV',
},
{
betreuteId: person.id,
bezeichnung: `${KENNUNG} Haftpflicht`,
gesellschaft: `${KENNUNG} Allgemeine`,
art: 'HAFTPFLICHT',
status: 'AKTIV',
},
{
betreuteId: person.id,
bezeichnung: `${KENNUNG} Ohne Gesellschaft`,
gesellschaft: null,
art: 'LEBENSVERSICHERUNG',
status: 'AKTIV',
},
],
})
}
})
})
test.afterAll(async () => {
await aufraeumen()
})
test('wählt richtig vor und benennt die Hindernisse', async ({ page }) => {
await anmelden(page)
await page.goto('/vertraege/serienabfrage/seite')
// Die beiden anfragbaren sind vorausgewählt …
await expect(
page.locator('li', { hasText: `${KENNUNG} Leben` }).locator('input[type="checkbox"]'),
).toBeChecked()
await expect(
page.locator('li', { hasText: `${KENNUNG} Sterbegeld` }).locator('input[type="checkbox"]'),
).toBeChecked()
// … die ohne Gesellschaft steht mit Begründung da, ohne Haken …
const ohne = page.locator('li', { hasText: `${KENNUNG} Ohne Gesellschaft` })
await expect(ohne).toContainText('nachtragen')
await expect(ohne.locator('input[type="checkbox"]')).toHaveCount(0)
// … und die Haftpflicht taucht gar nicht erst auf.
await expect(page.locator('li', { hasText: `${KENNUNG} Haftpflicht` })).toHaveCount(0)
// Die Zusammenfassung steht auf dem Knopf, BEVOR etwas entsteht -
// und sie stimmt mit der Zahl der gesetzten Haken ueberein. Auf die
// absolute Zahl laesst sich nicht bauen: Der Demofall bringt eigene
// Vertraege mit.
const haken = await page.locator('input[type="checkbox"]:checked').count()
expect(haken).toBeGreaterThanOrEqual(2)
await expect(
page.getByRole('button', { name: new RegExp(`${haken} Schreiben erzeugen`) }),
).toBeVisible()
})
test('führt den Lauf durch: ZIP, Akte, Wiedervorlagen, Anfragedatum', async ({ page }) => {
await anmelden(page)
await page.goto('/vertraege/serienabfrage/seite')
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: /Schreiben erzeugen und herunterladen/ }).click(),
])
expect(download.suggestedFilename()).toMatch(/^\d{4}-\d{2}-\d{2}_Rueckkaufswert-Anfragen\.zip$/)
// Nebenwirkungen in der Datenbank: Anfragedatum, Dokumente, Fristen.
const stand = await mitDb(async (db) => {
const angefragt = await db.vertrag.count({
where: {
bezeichnung: { in: [`${KENNUNG} Leben`, `${KENNUNG} Sterbegeld`] },
rueckkaufswertAngefragtAm: { not: null },
},
})
const dokumente = await db.dokument.count({
where: { empfaenger: { contains: KENNUNG }, richtung: 'AUSGANG' },
})
const fristen = await db.frist.count({
where: { titel: { contains: KENNUNG }, typ: 'WIEDERVORLAGE' },
})
return { angefragt, dokumente, fristen }
})
// Beide betreute Personen haben die Verträge - angefragt wird aber nur
// bei der aktiven. Deshalb: genau 2 von 4.
expect(stand.angefragt).toBe(2)
expect(stand.dokumente).toBe(2)
expect(stand.fristen).toBe(2)
// Auf der Seite stehen die beiden jetzt als frisch angefragt.
await page.goto('/vertraege/serienabfrage/seite')
await expect(page.getByText(/bereits angefragt/).first()).toBeVisible()
})
test('erzeugt beim zweiten Lauf am selben Tag keine Dubletten', async ({ page }) => {
await anmelden(page)
await page.goto('/vertraege/serienabfrage/seite')
// Alles abwaehlen, dann NUR den frisch angefragten Vertrag anwaehlen.
// So prueft der Lauf genau den Dublettenschutz und fasst die uebrigen
// Vertraege nicht an.
const angehakte = page.locator('input[type="checkbox"]:checked')
while ((await angehakte.count()) > 0) {
await angehakte.first().uncheck()
}
const leben = page.locator('li', { hasText: `${KENNUNG} Leben` })
await leben.locator('input[type="checkbox"]').check()
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: /Schreiben erzeugen und herunterladen/ }).click(),
])
expect(download.suggestedFilename()).toContain('.zip')
// Der Dublettenschutz greift: weiterhin genau ein Dokument und eine
// Frist für diesen Vertrag.
const stand = await mitDb(async (db) => {
const dokumente = await db.dokument.count({
where: { empfaenger: `${KENNUNG} Lebensversicherung` },
})
const fristen = await db.frist.count({
where: { titel: { contains: `${KENNUNG} Leben` }, typ: 'WIEDERVORLAGE' },
})
return { dokumente, fristen }
})
expect(stand.dokumente).toBe(1)
expect(stand.fristen).toBe(1)
})
})