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
139 lines
5.1 KiB
TypeScript
139 lines
5.1 KiB
TypeScript
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')
|
|
})
|
|
})
|