Compare commits

..
2 Commits
Author SHA1 Message Date
florian.wilde 91592cbe88 Merge #33: Ein Jahr Kontobewegungen im Beispielfall
Behebt Issue #22: 107 Buchungen ueber zwoelf Monate im Demofall, mit
gerechnetem Kontostand. Dazu behoben: gGmbH fehlte in der Rechtsformliste
beim Zusammenfassen wiederkehrender Zahlungen.

Closes #22
2026-08-23 19:36:49 +00:00
Florian Wilde 18709a8e49 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
2026-08-23 21:35:46 +02:00
9 changed files with 520 additions and 13 deletions
+50 -1
View File
@@ -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
+7 -7
View File
@@ -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 · <commit>`.
- Unten links in der Seitenleiste steht `Version 1.10.1 · <commit>`.
---
@@ -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
+1 -1
View File
@@ -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))
---
+138
View File
@@ -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')
})
})
+1 -1
View File
@@ -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": {
+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.')
}
+4
View File
@@ -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',
+1 -1
View File
@@ -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:
+11
View File
@@ -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'),