Ein Jahr Kontobewegungen im Beispielfall

Der Demofall brachte Konten und Kontostaende mit, aber keine einzige
Buchung. Kassenbuch, Belegpflicht, Dauerzahlungen und die Erkennung
wiederkehrender Zahlungen liefen darin also leer - wer die Anwendung zum
ersten Mal ansah, musste erst eine CSV importieren.

Neu sind 107 Buchungen ueber zwoelf Monate, erzeugt aus einem Muster
statt einzeln abgetippt: Rente, Betriebsrente und Pflegekasse als
Einnahmen, Heimentgelt, Barbetrag, Sterbegeldbeitrag, Telefon und
Kontofuehrung monatlich, dazu Rundfunkbeitrag und Grundsteuer
vierteljaehrlich sowie eine Handvoll Einzelposten.

Was der Fall damit zeigt:
- Das Heimentgelt steht in drei Schreibweisen auf dem Auszug und wird
  trotzdem als eine monatliche Zahlung gefuehrt.
- Genau eine Buchung hat weder Quittung noch Begruendung. Eine, nicht
  zwanzig: Die Liste soll etwas zu zeigen haben und nicht rot leuchten.
- Die Sterbegeld-Lastschriften bleiben unzugeordnet, damit sich der
  Handgriff "zwoelf Zahlungen als belegt zuordnen" ausprobieren laesst.
  Der Beitrag steigt im Mai von 18,40 auf 19,80 - damit auch die
  Abweichungspruefung sichtbar wird.
- Der Kontostand zum 31.07. wird aus Anfangsstand und Buchungen
  gerechnet, nicht behauptet. Ein Beispielfall, der sich selbst
  widerspricht, waere schlimmer als einer ohne Buchungen.

Zwei Funde beim Bauen:

Der Unique-Index auf (kontoId, importReferenz) hat einen echten Fehler in
den Daten gefangen: Zwei Renten am Monatsersten ergaben dieselbe
Referenz. Genau dafuer ist er da.

"gGmbH" fehlte in der Rechtsformliste. Dadurch galten "Seniorenzentrum
St. Katharina" und "... gGmbH" als zwei Empfaenger - und die Gruppe
zerfiel ausgerechnet in dem Fachgebiet, in dem fast jeder Traeger eine
gGmbH ist. Ergaenzt, mit Test.

Geprueft: ein Einheitstest fuer die Rechtsform, vier End-to-End-Tests am
Beispielfall - Umfang und Zeitraum, Kontostand gegen Buchungssumme, die
erkannten Gruppen und das Kassenbuch.

Refs #22
This commit is contained in:
Florian Wilde
2026-08-23 21:35:46 +02:00
parent b6b579a04d
commit 18709a8e49
9 changed files with 520 additions and 13 deletions
+138
View File
@@ -0,0 +1,138 @@
import { expect, test } from '@playwright/test'
import { PrismaClient } from '@prisma/client'
import { anmelden } from './hilfen'
/**
* Der Demo-Fall mit Kontobewegungen — gegen die echte Anwendung.
*
* Was hier geprüft wird, ist kein Verhalten der Anwendung, sondern die
* Brauchbarkeit der Beispieldaten: Wer die Anwendung zum ersten Mal
* ansieht, soll auf den Finanzseiten etwas vorfinden und nicht erst
* eine CSV importieren müssen.
*
* Die Prüfungen sind bewusst grob gehalten — sie sollen anschlagen, wenn
* der Demo-Fall leer läuft, und nicht bei jeder geänderten Zahl.
*/
async function mitDb<T>(arbeit: (db: PrismaClient) => Promise<T>): Promise<T> {
const db = new PrismaClient()
try {
return await arbeit(db)
} finally {
await db.$disconnect()
}
}
test.describe('Demo-Fall', () => {
test('bringt ein Jahr Kontobewegungen mit', async () => {
const zahlen = await mitDb(async (db) => {
const betreute = await db.betreute.findFirst({
where: { vorname: 'Erika', nachname: 'Musterfrau' },
select: { id: true },
})
if (!betreute) return null
const buchungen = await db.buchung.findMany({
where: { betreuteId: betreute.id, geloeschtAm: null },
select: { datum: true },
orderBy: { datum: 'asc' },
})
const ohneNachweis = await db.buchung.count({
where: { betreuteId: betreute.id, belegId: null, belegFehltBegruendung: null },
})
return { buchungen, ohneNachweis }
})
test.skip(zahlen === null, 'Kein Demo-Fall in dieser Datenbank.')
if (!zahlen) return
expect(zahlen.buchungen.length).toBeGreaterThan(50)
// Über mindestens zwölf Monate verteilt, nicht alle an einem Tag.
const erste = zahlen.buchungen[0]!.datum
const letzte = zahlen.buchungen[zahlen.buchungen.length - 1]!.datum
const monate =
(letzte.getUTCFullYear() - erste.getUTCFullYear()) * 12 +
(letzte.getUTCMonth() - erste.getUTCMonth())
expect(monate).toBeGreaterThanOrEqual(11)
// Genau ein Fall ohne Beleg und ohne Begründung — sonst hätte die
// Belegpflicht nichts zu zeigen, oder zu viel.
expect(zahlen.ohneNachweis).toBe(1)
})
test('der Kontostand stimmt mit der Summe der Buchungen überein', async () => {
const ergebnis = await mitDb(async (db) => {
const konto = await db.konto.findFirst({
where: { art: 'GIRO', betreute: { vorname: 'Erika', nachname: 'Musterfrau' } },
select: { id: true },
})
if (!konto) return null
const staende = await db.saldostand.findMany({
where: { kontoId: konto.id },
orderBy: { stichtag: 'asc' },
})
if (staende.length < 2) return null
const [anfang, ende] = [staende[0]!, staende[staende.length - 1]!]
const bewegungen = await db.buchung.findMany({
where: {
kontoId: konto.id,
geloeschtAm: null,
datum: { gt: anfang.stichtag, lte: ende.stichtag },
},
select: { betragCent: true, richtung: true },
})
const summe = bewegungen.reduce(
(wert, b) => (b.richtung === 'EINNAHME' ? wert + b.betragCent : wert - b.betragCent),
0n,
)
return { erwartet: anfang.betragCent + summe, tatsaechlich: ende.betragCent }
})
test.skip(ergebnis === null, 'Kein Demo-Fall mit zwei Kontoständen.')
if (!ergebnis) return
// Ein Demo-Fall, der sich selbst widerspricht, ist schlimmer als
// einer ohne Buchungen: Man rechnet nach und verliert das Vertrauen.
expect(ergebnis.tatsaechlich).toBe(ergebnis.erwartet)
})
test('die Erkennung wiederkehrender Zahlungen findet mehrere Gruppen', async ({ page }) => {
await anmelden(page)
await page.goto('/finanzen/wiederkehrend')
const inhalt = await page.locator('body').innerText()
// Monatliches und Vierteljährliches, beides erkannt.
expect(inhalt).toContain('Seniorenzentrum')
expect(inhalt).toContain('TeleCare')
expect(inhalt).toContain('Beitragsservice')
expect(inhalt).toContain('monatlich')
expect(inhalt).toContain('vierteljährlich')
// Der eigentliche Vorführeffekt: Das Heim steht in drei Schreibweisen
// auf dem Auszug und wird trotzdem als eine Zahlung geführt.
expect(inhalt).toContain('3 Schreibweisen zusammengefasst')
// Die Sterbekasse steht im zweiten Abschnitt: Zu ihr gibt es einen
// Vertrag, die Zahlungen sind ihm aber noch nicht zugeordnet. Damit
// lässt sich der Handgriff aus der Dauerzahlungs-Zuordnung am
// Demo-Fall ausprobieren, statt ihn nur zu beschreiben.
expect(inhalt).toContain('Schon einem Vertrag zugeordnet')
expect(inhalt).toContain('Nordheim Sterbekasse')
expect(inhalt).toContain('als belegt zuordnen')
})
test('das Kassenbuch zeigt die Buchungen und die fehlende Quittung', async ({ page }) => {
await anmelden(page)
await page.goto('/finanzen/buchungen')
const inhalt = await page.locator('body').innerText()
expect(inhalt).toContain('Heimentgelt')
// Die eine Buchung ohne Quittung und ohne Begründung.
expect(inhalt).toContain('Modehaus Lindner')
})
})