Files
Florian Wilde e7006c894e feat: Konten, Kassenbuch, Vermoegen und die beiden Pflicht-PDFs
Etappe 4. Aus diesen Daten entstehen das Vermoegensverzeichnis nach
§ 1835 BGB und die Rechnungslegung nach §§ 1859, 1865 BGB - beides geht
ans Betreuungsgericht, beides ohne dass eine Zahl abgetippt wird.

Betreuungsjahr n laeuft von bestellungAm + (n-1) Jahre bis
bestellungAm + n Jahre - 1 Tag. Bei Bestellung am 29. Februar endet Jahr
1 am 27.02. und Jahr 2 beginnt am 28.02.: lueckenlos und ohne
Ueberschneidung, per Test abgesichert.

Rechenprobe der Rechnungslegung: Der Anfangsbestand ist der juengste
Saldostand bis zum VORTAG des Zeitraumbeginns - ein Saldostand gilt als
Bestand am Tagesende, sonst zaehlte der erste Tag doppelt. Geht die
Probe nicht auf, wird die Differenz im PDF ausgewiesen und jeder
fehlende Saldostand namentlich benannt. Nie geglaettet: eine still
korrigierte Rechnungslegung ist gegenueber dem Gericht wertlos.

Posten ohne Wertstand zum Stichtag erscheinen als Luecke, nicht als
0,00 EUR - und Werte aus der Zukunft werden nie herangezogen. Konten
kommen ueber ihren Saldostand ins Verzeichnis; Vermoegensposten der Art
KONTO werden dort uebersprungen, sonst stuende das Geld doppelt drin.

Der CSV- und CAMT-Import raet KEINE Kategorien, sondern bucht alles auf
"Sonstige". Eine falsch geratene Kategorie faellt in der Jahresuebersicht
niemandem auf und verfaelscht den Bericht. Fremdwaehrung wird abgelehnt
statt umgerechnet. Der XML-Leser weist DOCTYPE ab (XXE).

68 Tests, darunter echte IBAN-Pruefziffern und die Rundung: es darf
nirgends ein Cent verschwinden.

Etappe: 4
Phase: Geld
2026-08-22 06:21:45 +02:00

155 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Schema-Wünsche aus Etappe 4 „Geld“
Beim Bau des Finanzmoduls sind Stellen aufgefallen, an denen das
Datenmodell etwas anders aussehen müsste. `prisma/schema.prisma` wurde
**nicht** angefasst. Sortiert nach Dringlichkeit.
---
## 1. Umbuchungen kennen ihr Gegenkonto nicht
**Modell:** `Buchung`
Eine Umbuchung zwischen zwei eigenen Konten – Giro aufs Sparbuch – lässt
sich derzeit nur als zwei getrennte Buchungen abbilden. Im Modell fehlt
die Angabe, wohin das Geld gegangen ist. Für die Rechenprobe ist das
folgenlos (Umbuchungen sind weder Einnahme noch Ausgabe und werden
herausgerechnet), für den Nutzer aber unbequem: Er muss zweimal
erfassen, und niemand prüft, ob beide Seiten zusammenpassen.
```prisma
model Buchung {
gegenkontoId String?
gegenkonto Konto? @relation("Umbuchungsgegenkonto", fields: [gegenkontoId], references: [id], onDelete: SetNull)
}
```
Damit ließe sich eine Umbuchung als **eine** Zeile erfassen und beim
Bilden der Kontostände auf beiden Seiten berücksichtigen.
---
## 2. Dublettenschutz greift nicht bei Buchungen ohne Konto
**Modell:** `Buchung`, `@@unique([kontoId, importReferenz])`
In PostgreSQL ist `NULL` nie gleich `NULL`. Die zusammengesetzte
Eindeutigkeit schützt deshalb nur, solange `kontoId` gesetzt ist. Bei
einem Import ohne Konto – derzeit über die Oberfläche nicht möglich,
aber im Modell erlaubt – könnte derselbe Umsatz beliebig oft angelegt
werden.
Zwei mögliche Auswege:
- `kontoId` bei Buchungen mit `importReferenz` verpflichtend machen
(Prüfung in der Anwendung, Constraint im Schema nicht ausdrückbar), oder
- eine partielle Eindeutigkeit über Roh-SQL ergänzen:
```sql
CREATE UNIQUE INDEX buchung_import_ohne_konto
ON buchung (betreute_id, import_referenz)
WHERE konto_id IS NULL AND import_referenz IS NOT NULL;
```
Die Anwendung erzwingt derzeit ein Konto beim Import; der Schutz hängt
also allein an der Oberfläche.
---
## 3. Saldostand und Wertstand haben keinen Papierkorb
**Modelle:** `Saldostand`, `Wertstand`
Beide Modelle haben kein `geloeschtAm`. Löschen ist damit endgültig –
im Gegensatz zu allem anderen im System. Das ist an dieser Stelle
besonders unangenehm: Wird ein Wertstand entfernt, lässt sich eine
bereits beim Gericht eingereichte Vermögensübersicht nicht mehr
nachbilden. Die Oberfläche warnt deshalb ausdrücklich davor, aber ein
Papierkorb wäre die bessere Absicherung.
```prisma
model Saldostand {
geloeschtAm DateTime?
}
model Wertstand {
geloeschtAm DateTime?
}
```
Nebenwirkung, die dabei zu bedenken ist: die bestehenden
`@@unique([kontoId, stichtag])` bzw. `@@unique([vermoegenspostenId, stichtag])`
würden dann auch gelöschte Einträge blockieren. Sie müssten auf
partielle Indizes mit `WHERE geloescht_am IS NULL` umgestellt werden.
---
## 4. Wer hat den Stand erfasst?
**Modelle:** `Saldostand`, `Wertstand`
Beide tragen nur `erstelltAm`, keinen Verweis auf den Benutzer. Bei
mehreren Betreuern oder Mitlesern mit Schreibrecht steht im Datensatz
selbst nicht, wer die Zahl eingetragen hat. Das Audit-Log beantwortet
die Frage, aber nur über eine gesonderte Recherche.
```prisma
erstelltVonId String?
erstelltVon User? @relation(fields: [erstelltVonId], references: [id], onDelete: SetNull)
```
---
## 5. Indizes für die Auswertungen
**Modelle:** `Buchung`, `Saldostand`
Die Berichte lesen Buchungen fast immer entweder nach Konto und Zeitraum
oder gefiltert auf fehlende Belege. Dafür gibt es bisher keine passenden
Indizes:
```prisma
model Buchung {
@@index([kontoId, datum])
@@index([betreuteId, belegId])
}
```
Bei den erwarteten Datenmengen (einige tausend Buchungen je betreuter
Person) ist das noch nicht spürbar; bei einem Import mehrerer Jahre
Kontoauszüge wird es das.
---
## 6. Fremdwährung wird abgelehnt, nicht abgebildet
**Modelle:** `Konto`, `Buchung`, `Saldostand`, `Wertstand`
Es gibt kein Währungsfeld. Alle Beträge sind Euro-Cent. Der CSV- und
CAMT-Import überspringt deshalb Zeilen in fremder Währung und meldet das
dem Nutzer, statt umzurechnen – ein umgerechneter Betrag im
Kassenbuch wäre in der Rechnungslegung eine Behauptung ohne Beleg.
Das ist als bewusste Entscheidung dokumentiert und **kein** Wunsch nach
einem Währungsfeld. Es steht hier nur, damit die Einschränkung nicht
später für ein Versehen gehalten wird.
---
## 7. Der Betrag einer Buchung sollte positiv sein
**Modell:** `Buchung.betragCent`
Fachlich ist der Betrag immer positiv; die Richtung steht in
`richtung`. Ein negativer Betrag bei `richtung = AUSGABE` würde die
Vorzeichen zweimal drehen und die Summen still verfälschen. Die
Anwendung prüft das beim Speichern (`betragFeld({ nurPositiv: true })`),
in der Datenbank ist es nicht abgesichert. Ein Check-Constraint über
Roh-SQL wäre die Ergänzung:
```sql
ALTER TABLE buchung ADD CONSTRAINT buchung_betrag_positiv CHECK (betrag_cent > 0);
```
Vor einer solchen Migration müssten Altdaten geprüft werden.