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
This commit is contained in:
+50
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.')
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user