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:
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user