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