From 18709a8e499e7100896207793ecc5bfe33721d0f Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 23 Aug 2026 21:35:46 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 51 ++++- docs/INSTALLATION.md | 14 +- docs/ROADMAP.md | 2 +- e2e/demofall.spec.ts | 138 +++++++++++++ package.json | 2 +- prisma/seed.ts | 309 +++++++++++++++++++++++++++++- src/lib/finanzen/wiederkehrend.ts | 4 + src/lib/version.ts | 2 +- tests/wiederkehrend.test.ts | 11 ++ 9 files changed, 520 insertions(+), 13 deletions(-) create mode 100644 e2e/demofall.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b978127..eecf44c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,54 @@ Wiederherstellung*. _Noch nichts._ +## [1.10.1] – 2026-08-23 + +### Geändert + +- **Der Beispielfall hat jetzt ein Jahr Kontobewegungen** + ([#22](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/22)). + Bisher brachte er Konten und Kontostände 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 über zwölf Monate: Rente, Betriebsrente und + Pflegekasse als Einnahmen, Heimentgelt, Barbetrag, Sterbegeldbeitrag, + Telefon und Kontoführung als monatliche Ausgaben, dazu Rundfunkbeitrag + und Grundsteuer vierteljährlich sowie eine Handvoll Einzelposten. + + Das betrifft nur neue Installationen mit `SEED_DEMO=true` — an einem + bestehenden Bestand ändert sich nichts. + +### Behoben + +- **„gGmbH" wurde nicht als Rechtsform erkannt.** Beim Zusammenfassen + wiederkehrender Zahlungen galten „Seniorenzentrum St. Katharina" und + „Seniorenzentrum St. Katharina gGmbH" als zwei verschiedene Empfänger — + und damit zerfiel die Gruppe genau dort, wo sie am meisten zählt: Heime, + Sozialdienste und Träger sind fast durchweg gGmbH. + +### Was der Beispielfall jetzt zeigt + +- Das Heimentgelt steht in **drei Schreibweisen** auf dem Auszug und wird + trotzdem als eine monatliche Zahlung von 2.489 € geführt. +- Genau **eine** Buchung hat weder Quittung noch Begründung — damit die + Belegpflicht etwas zu zeigen hat, aber die Liste nicht rot leuchtet. +- Die Sterbegeld-Lastschriften sind bewusst **nicht** zugeordnet: So lässt + sich der Handgriff „zwölf Zahlungen als belegt zuordnen" ausprobieren. + Der Beitrag steigt im Mai von 18,40 € auf 19,80 €, damit auch die + Abweichungsprüfung sichtbar wird. +- Der Kontostand zum 31.07.2026 wird aus dem Anfangsstand und den + Buchungen **gerechnet**, nicht behauptet. Ein Beispielfall, der sich + selbst widerspricht, wäre schlimmer als einer ohne Buchungen. + +### Geprüft + +- Ein Einheitstest für die gGmbH-Erkennung. +- Vier End-to-End-Tests am Beispielfall: Umfang und Zeitraum der + Buchungen, die Übereinstimmung von Kontostand und Buchungssumme, die + erkannten Zahlungsgruppen und das Kassenbuch. + ## [1.10.0] – 2026-08-23 ### Hinzugefügt @@ -1078,7 +1126,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). --- -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...HEAD +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.1...HEAD +[1.10.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...v1.10.1 [1.10.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.9.0...v1.10.0 [1.9.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.8.0...v1.9.0 [1.8.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.7.0...v1.8.0 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 1ed41c3..c9e8bd8 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -131,14 +131,14 @@ cd /opt/betreuungs-cockpit # Auf die letzte veröffentlichte Fassung wechseln, # nicht auf den laufenden Entwicklungsstand -git checkout v1.10.0 +git checkout v1.10.1 ``` ### Kontrolle ```bash ls docker-compose.yml Dockerfile .env.example # alle drei da -git describe --tags # v1.10.0 +git describe --tags # v1.10.1 ``` --- @@ -299,7 +299,7 @@ betreuungs-cockpit-sicherung-1 Up ```bash # Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast) curl -s http://127.0.0.1:3000/api/gesundheit -# {"status":"bereit","version":"1.10.0","commit":"..."} +# {"status":"bereit","version":"1.10.1","commit":"..."} ``` Kommt hier nichts, sieh in die Logs: @@ -649,7 +649,7 @@ Erst **vom Proxy-Server aus** — kommt er überhaupt an? ```bash curl -s http://10.0.0.5:3000/api/gesundheit -# {"status":"bereit","version":"1.10.0",...} +# {"status":"bereit","version":"1.10.1",...} ``` Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der @@ -700,7 +700,7 @@ Zum Schluss: **Fristen → Pflichtfristen ableiten**. - Du kommst mit E-Mail, Passwort und dem Code aus der App herein. - Unter **Fristen** stehen Einträge mit Paragraphenbezug. - Unter **Protokoll** siehst du deine eigene Anmeldung. -- Unten links in der Seitenleiste steht `Version 1.10.0 · `. +- Unten links in der Seitenleiste steht `Version 1.10.1 · `. --- @@ -875,7 +875,7 @@ alles so vor wie zuvor. | Aufruf | Wirkung | |---|---| | `./scripts/update.sh` | auf die neueste veröffentlichte Fassung | -| `./scripts/update.sh v1.10.0` | auf eine bestimmte Fassung | +| `./scripts/update.sh v1.10.1` | auf eine bestimmte Fassung | | `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern | | `./scripts/update.sh --ja` | ohne Rückfrage (für cron) | | `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen | @@ -913,7 +913,7 @@ git fetch --tags git log --oneline HEAD..origin/main # 3. Umschalten -git checkout v1.10.0 +git checkout v1.10.1 # 4. Neu bauen und starten GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b070f36..2606f75 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -9,7 +9,7 @@ Zeit spart. Er steht weit unten, wenn er nur schön wäre. Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb als wichtiger herausstellt, rutscht nach oben. -Stand: 23.08.2026 · Aktuelle Fassung: **1.10.0** ([CHANGELOG](../CHANGELOG.md)) +Stand: 23.08.2026 · Aktuelle Fassung: **1.10.1** ([CHANGELOG](../CHANGELOG.md)) --- diff --git a/e2e/demofall.spec.ts b/e2e/demofall.spec.ts new file mode 100644 index 0000000..e62e727 --- /dev/null +++ b/e2e/demofall.spec.ts @@ -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(arbeit: (db: PrismaClient) => Promise): Promise { + 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') + }) +}) diff --git a/package.json b/package.json index 4665a02..153f30b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betreuungs-cockpit", - "version": "1.10.0", + "version": "1.10.1", "private": true, "description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung", "engines": { diff --git a/prisma/seed.ts b/prisma/seed.ts index c5ae85e..b013b46 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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.') } diff --git a/src/lib/finanzen/wiederkehrend.ts b/src/lib/finanzen/wiederkehrend.ts index 8ab3b13..bf320fb 100644 --- a/src/lib/finanzen/wiederkehrend.ts +++ b/src/lib/finanzen/wiederkehrend.ts @@ -122,6 +122,10 @@ export type Zahlungsgruppe = { const RECHTSFORMEN = [ 'gmbh & co kg', 'gmbh und co kg', + // Die haeufigste Rechtsform in diesem Fachgebiet ueberhaupt: Heime, + // Sozialdienste und Traeger sind fast durchweg gGmbH. Fehlte sie, + // zerfiele die Zusammenfassung genau dort, wo sie gebraucht wird. + 'ggmbh', 'gmbh', 'mbh', 'ohg', diff --git a/src/lib/version.ts b/src/lib/version.ts index ef10ea8..94a0cee 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -17,7 +17,7 @@ */ /** Muss zur "version" in der package.json passen. */ -export const VERSION = '1.10.0' +export const VERSION = '1.10.1' /** * Kurzer Git-Stand, beim Bauen hineingereicht: diff --git a/tests/wiederkehrend.test.ts b/tests/wiederkehrend.test.ts index 0594b70..b758697 100644 --- a/tests/wiederkehrend.test.ts +++ b/tests/wiederkehrend.test.ts @@ -75,6 +75,17 @@ describe('Schreibweisen vergleichbar machen', () => { expect(normalisieren('Debeka AG')).toBe(normalisieren('Debeka')) }) + it('kennt die gGmbH', () => { + // Heime, Sozialdienste und Träger sind fast durchweg gGmbH — ohne + // sie zerfiele die Zusammenfassung genau dort, wo sie zählt. + expect(normalisieren('Seniorenzentrum St. Katharina gGmbH')).toBe( + normalisieren('Seniorenzentrum St. Katharina'), + ) + expect(normalisieren('SENIORENZENTRUM ST KATHARINA GGMBH')).toBe( + normalisieren('Seniorenzentrum St. Katharina'), + ) + }) + it('entfernt auch mehrteilige Rechtsformen', () => { expect(normalisieren('Stadtwerke Bamberg GmbH & Co KG')).toBe( normalisieren('Stadtwerke Bamberg'), -- 2.54.0