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
+307 -2
View File
@@ -22,7 +22,7 @@
*
* Der Seed ist idempotent: mehrfaches Ausfuehren aendert nichts.
*/
import { PrismaClient } from '@prisma/client'
import { PrismaClient, type BuchungKategorie, type Prisma } from '@prisma/client'
import { hash } from '@node-rs/argon2'
import { createCipheriv, randomBytes } from 'node:crypto'
@@ -263,9 +263,11 @@ async function demofall(userId: string) {
art: 'GIRO',
ibanVerschluesselt: verschluesseln('DE02773501100123456789', 'ibanVerschluesselt'),
saldostaende: {
// Nur der Anfangsstand. Der spaetere wird weiter unten aus den
// Buchungen gerechnet - ein Demofall, der sich selbst
// widerspricht, waere schlimmer als einer ohne Buchungen.
create: [
{ stichtag: datum('2026-02-14'), betragCent: cent(4218.36), quelle: 'Kontoauszug 02/2026' },
{ stichtag: datum('2026-07-31'), betragCent: cent(2967.11), quelle: 'Kontoauszug 07/2026' },
],
},
},
@@ -523,6 +525,309 @@ async function demofall(userId: string) {
})
}
// --- Kontobewegungen ----------------------------------------------------
// Zwoelf Monate auf dem Girokonto. Erzeugt aus einem Muster und nicht
// einzeln abgetippt: So bleibt der Fall aenderbar, und die Betraege
// widersprechen sich nicht.
//
// Die Gegenstellen sind absichtlich uneinheitlich geschrieben. Genau
// damit muss die Zusammenfassung wiederkehrender Zahlungen umgehen - auf
// einem Kontoauszug steht dieselbe Stelle auch mal in Grossbuchstaben.
type Buchungsvorlage = {
tag: number
betrag: number
richtung: 'EINNAHME' | 'AUSGABE'
kategorie: BuchungKategorie
gegenstellen: string[]
zweck: string
}
const MONATLICH: Buchungsvorlage[] = [
{
tag: 1,
betrag: 1284.17,
richtung: 'EINNAHME',
kategorie: 'RENTE',
gegenstellen: [
'Deutsche Rentenversicherung Bund',
'DRV BUND',
'Deutsche Rentenversich. Bund',
],
zweck: 'Altersrente',
},
{
tag: 1,
betrag: 96.4,
richtung: 'EINNAHME',
kategorie: 'RENTE',
gegenstellen: ['Zusatzversorgungskasse Bayern', 'ZVK Bayern'],
zweck: 'Betriebsrente',
},
{
tag: 3,
betrag: 1262,
richtung: 'EINNAHME',
kategorie: 'PFLEGEKASSE',
gegenstellen: ['Pflegekasse bei der AOK Bayern', 'AOK Bayern - Pflegekasse'],
zweck: 'Leistung vollstationaere Pflege, Pflegegrad 3',
},
{
tag: 5,
betrag: 2489,
richtung: 'AUSGABE',
kategorie: 'HEIMKOSTEN',
gegenstellen: [
'Seniorenzentrum St. Katharina',
'SENIORENZENTRUM ST KATHARINA',
'Seniorenzentrum St. Katharina gGmbH',
],
zweck: 'Heimentgelt',
},
{
tag: 5,
betrag: 135,
richtung: 'AUSGABE',
kategorie: 'BARBETRAG',
// Eigene Gegenstelle, nicht das Heim: Sonst faellt der Barbetrag
// mit dem Heimentgelt in eine Gruppe, und aus zwei sauberen
// monatlichen Zahlungen wird eine "schwankende".
gegenstellen: ['Barauszahlung Verwahrgeldkonto'],
zweck: 'Barbetrag zur persoenlichen Verfuegung',
},
{
tag: 15,
betrag: 18.4,
richtung: 'AUSGABE',
kategorie: 'VERSICHERUNG',
gegenstellen: ['Nordheim Sterbekasse VVaG', 'NORDHEIM STERBEKASSE', 'Nordheim Sterbekasse'],
zweck: 'Beitrag Sterbegeldversicherung SG-220417',
},
{
tag: 20,
betrag: 24.9,
richtung: 'AUSGABE',
kategorie: 'TELEFON_INTERNET',
gegenstellen: ['TeleCare Kommunikation GmbH', 'TELECARE KOMMUNIKATION'],
zweck: 'Telefon und Fernsehen im Zimmer',
},
{
tag: 28,
betrag: 4.9,
richtung: 'AUSGABE',
kategorie: 'BANKGEBUEHREN',
gegenstellen: ['Sparkasse Bayreuth'],
zweck: 'Kontofuehrung',
},
]
/** Zwoelf Monate bis einschliesslich Juli 2026. */
const MONATE = [
'2025-08',
'2025-09',
'2025-10',
'2025-11',
'2025-12',
'2026-01',
'2026-02',
'2026-03',
'2026-04',
'2026-05',
'2026-06',
'2026-07',
]
const buchungen: Prisma.BuchungCreateManyInput[] = []
MONATE.forEach((monat, lauf) => {
MONATLICH.forEach((vorlage, stelle) => {
// Die Sterbegeld-Lastschrift steigt im Mai 2026. Daran wird die
// Pruefung beim Zuordnen einer Dauerzahlung sichtbar: Der Betrag
// weicht vom hinterlegten Beitrag ab.
const betrag =
vorlage.kategorie === 'VERSICHERUNG' && monat >= '2026-05' ? 19.8 : vorlage.betrag
buchungen.push({
betreuteId: betreute.id,
kontoId: giro.id,
datum: datum(`${monat}-${String(vorlage.tag).padStart(2, '0')}`),
betragCent: cent(betrag),
richtung: vorlage.richtung,
kategorie: vorlage.kategorie,
gegenstelle: vorlage.gegenstellen[lauf % vorlage.gegenstellen.length],
verwendungszweckVerschluesselt: verschluesseln(
vorlage.zweck,
'verwendungszweckVerschluesselt',
),
// Wiederkehrende Zahlungen brauchen keinen Einzelbeleg - der
// Bescheid bzw. der Vertrag dahinter ist der Nachweis.
belegFehltBegruendung:
vorlage.richtung === 'EINNAHME'
? 'Laufender Bezug, Nachweis ist der Bescheid.'
: 'Wiederkehrende Zahlung aus laufendem Vertrag.',
// Die Stelle in der Vorlagenliste gehoert in die Referenz: Zwei
// Renten am Monatsersten haetten sonst dieselbe, und der
// Unique-Index gegen Doppelbuchungen schlaegt zu Recht zu.
importReferenz: `demo-${monat}-${stelle}-${vorlage.kategorie}`,
})
})
})
// Vierteljaehrliches und Einmaliges - damit nicht alles im Monatsraster
// liegt und die Belegpflicht etwas zu zeigen hat.
const EINZELN: {
datum: string
betrag: number
richtung: 'EINNAHME' | 'AUSGABE'
kategorie: BuchungKategorie
gegenstelle: string
zweck: string
begruendung?: string
}[] = [
{
datum: '2025-08-15',
betrag: 55.08,
richtung: 'AUSGABE',
kategorie: 'RUNDFUNKBEITRAG',
gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice',
zweck: 'Rundfunkbeitrag 3. Quartal 2025',
begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.',
},
{
datum: '2025-11-15',
betrag: 55.08,
richtung: 'AUSGABE',
kategorie: 'RUNDFUNKBEITRAG',
gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice',
zweck: 'Rundfunkbeitrag 4. Quartal 2025',
begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.',
},
{
datum: '2026-02-15',
betrag: 55.08,
richtung: 'AUSGABE',
kategorie: 'RUNDFUNKBEITRAG',
gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice',
zweck: 'Rundfunkbeitrag 1. Quartal 2026',
begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.',
},
{
datum: '2026-05-15',
betrag: 55.08,
richtung: 'AUSGABE',
kategorie: 'RUNDFUNKBEITRAG',
gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice',
zweck: 'Rundfunkbeitrag 2. Quartal 2026',
begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.',
},
{
datum: '2026-02-16',
betrag: 187.5,
richtung: 'AUSGABE',
kategorie: 'GRUNDSTEUER',
gegenstelle: 'Gemeinde Bindlach',
zweck: 'Grundsteuer B, 1. Rate 2026',
begruendung: 'Bescheid der Gemeinde liegt in der Akte.',
},
{
datum: '2026-05-16',
betrag: 187.5,
richtung: 'AUSGABE',
kategorie: 'GRUNDSTEUER',
gegenstelle: 'Gemeinde Bindlach',
zweck: 'Grundsteuer B, 2. Rate 2026',
begruendung: 'Bescheid der Gemeinde liegt in der Akte.',
},
{
datum: '2026-01-19',
betrag: 240,
richtung: 'AUSGABE',
kategorie: 'IMMOBILIE_BETRIEBSKOSTEN',
gegenstelle: 'Stadtwerke Bayreuth',
zweck: 'Abschlag Strom und Wasser, Haus Bindlach',
begruendung: 'Jahresabrechnung steht noch aus.',
},
{
datum: '2026-03-11',
betrag: 10,
richtung: 'AUSGABE',
kategorie: 'ZUZAHLUNG',
gegenstelle: 'Apotheke am Markt',
zweck: 'Zuzahlung Rezept',
begruendung: 'Quittung liegt in der Akte.',
},
// Der Fall fuer die Belegpflicht: keine Quittung und keine Begruendung.
// Genau so sieht eine Buchung aus, die noch Arbeit macht.
{
datum: '2026-04-08',
betrag: 89.95,
richtung: 'AUSGABE',
kategorie: 'BEKLEIDUNG',
gegenstelle: 'Modehaus Lindner',
zweck: 'Winterjacke und zwei Nachthemden',
},
{
datum: '2026-06-17',
betrag: 23,
richtung: 'AUSGABE',
kategorie: 'SONSTIGE_AUSGABE',
gegenstelle: 'Friseur im Haus',
zweck: 'Friseur',
begruendung: 'Bar aus dem Barbetrag bezahlt, keine Quittung ausgestellt.',
},
{
datum: '2026-07-02',
betrag: 62.4,
richtung: 'EINNAHME',
kategorie: 'ZINSEN',
gegenstelle: 'Sparkasse Bayreuth',
zweck: 'Zinsgutschrift',
begruendung: 'Der Kontoauszug ist der Nachweis.',
},
]
for (const eintrag of EINZELN) {
buchungen.push({
betreuteId: betreute.id,
kontoId: giro.id,
datum: datum(eintrag.datum),
betragCent: cent(eintrag.betrag),
richtung: eintrag.richtung,
kategorie: eintrag.kategorie,
gegenstelle: eintrag.gegenstelle,
verwendungszweckVerschluesselt: verschluesseln(
eintrag.zweck,
'verwendungszweckVerschluesselt',
),
belegFehltBegruendung: eintrag.begruendung ?? null,
importReferenz: `demo-${eintrag.datum}-${eintrag.kategorie}`,
})
}
await db.buchung.createMany({ data: buchungen })
// Der Kontostand zum 31.07.2026 wird gerechnet, nicht behauptet: Anfangs-
// stand plus alles, was seither ueber das Konto gelaufen ist. Sonst
// widerspraeche der Demofall sich selbst, sobald jemand nachrechnet.
const bewegungCent = buchungen
.filter((eintrag) => {
const tag = eintrag.datum as Date
return tag > datum('2026-02-14') && tag <= datum('2026-07-31')
})
.reduce((summe, eintrag) => {
const betrag = eintrag.betragCent as bigint
return eintrag.richtung === 'EINNAHME' ? summe + betrag : summe - betrag
}, 0n)
await db.saldostand.create({
data: {
kontoId: giro.id,
stichtag: datum('2026-07-31'),
betragCent: cent(4218.36) + bewegungCent,
quelle: 'Kontoauszug 07/2026',
},
})
console.log('Demofall "Erika Musterfrau" angelegt.')
}