Phase 9: Kassenverwaltung

Kassenbuch, Mitgliedsbeitraege, Rechnungen an Dritte, Kassenpruefung und
die Auswertungen fuer die Jahreshauptversammlung.

Die drei Festlegungen aus Phase-9.md sitzen in der Datenbank, nicht im
Router: eine festgeschriebene Buchung ist unveraenderbar, ein geprueftes
Jahr nimmt kein Datum mehr an, und gerechnet wird mit Prisma.Decimal.
Korrigiert wird ueber eine Gegenbuchung mit Verweis - beide Buchungen
bleiben stehen, damit im Kassenbuch zu sehen ist, dass etwas berichtigt
wurde.

Neun Modelle, drei Trigger, zwoelf CHECKs, sechs Router, fuenf Ansichten.
Dazu der Bankimport fuer CAMT.053 und MT940 ohne Fremdbibliothek, der
Kategorievorschlag aus dem eigenen Buchungsbestand und die Bruecke von
Phase 8: aus einem gegengezeichneten Beleg wird eine Buchung, das Foto
haengt danach an ihr.

Nebenbei behoben: die Rechnungsnummer war ein Wettlauf, und eine
Organisation liess sich nicht mehr loeschen, sobald sie eine
festgeschriebene Buchung hatte - der Trigger kannte den
Aufbewahrungsschalter nicht.

562 Unit-Tests und 239 E2E-Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 19:23:34 +02:00
co-authored by Claude Opus 5
parent d70acad4cd
commit 4165334c5a
78 changed files with 12314 additions and 42 deletions
+158 -1
View File
@@ -6,11 +6,168 @@ Alle nennenswerten Änderungen an FLORIAN. Format nach
Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md).
## [Unveröffentlicht]
## [0.10.0] — 2026-09-04
Phase 9: Kassenverwaltung. Kassenbuch, Mitgliedsbeiträge, Rechnungen an
Dritte, Kassenprüfung und die Auswertungen für die Jahreshauptversammlung.
**Eine festgeschriebene Buchung ist unveränderbar.** Nicht, weil ein Zweig im
Router es so vorsieht, sondern weil die Datenbank es ablehnt. Korrigiert wird
über eine Gegenbuchung mit Verweis; beide Buchungen bleiben stehen, und im
Kassenbuch ist zu sehen, dass etwas berichtigt wurde. Genau das will ein
Kassenprüfer sehen — eine verschwundene Buchung dagegen kann er nicht prüfen.
**Der Kassenabschluss schließt seinen Zeitraum.** Danach wird keine Buchung
mit einem Datum darin mehr angenommen, auch keine nachträglich hineindatierte.
Ohne diese zweite Hälfte stimmte der geprüfte Bestand nach einem
Datumswechsel nicht mehr — und niemand hätte es bemerkt.
**Gerechnet wird mit `Prisma.Decimal`, nirgends mit Fließkomma.** `0.1 + 0.2`
ergibt in JavaScript 0.30000000000000004. In einer Kasse ist das kein
Rundungsfehler, sondern eine Differenz in der Prüfung, die niemand erklären
kann.
### Hinzugefügt
**Datenmodell**
- `Kassenkonto` (bar, Bank, Rücklage) mit Anfangsbestand. Der laufende Bestand
wird **gerechnet und nie gespeichert**: ein gespeicherter Bestand neben
einer nachgetragenen Buchung wäre falsch, ohne falsch auszusehen.
- `Buchungskategorie` als zweistufiger Baum, mit Kontonummer für den Export an
die Kämmerei.
- `Buchung` mit Datum, Richtung, immer positivem Betrag, Verwendungszweck,
Belegnummer, Beleg-Datei und dem Verweis auf die Gegenbuchung.
- `Beitragssatz` und `Beitrag` (offen, bezahlt, erlassen, gemahnt) mit
Mahnstufe und Erlassbegründung.
- `Rechnung` und `Rechnungsposition` mit Nummernkreis je Jahr. Die Menge hat
drei Nachkommastellen — Einsatzstunden werden viertelstündlich abgerechnet.
- `Kassenabschluss` und `Abschlussposten` mit Bestand laut Buch, gezähltem
Bestand, Differenz, zwei Prüfern und dem Bericht als PDF.
**Was die Datenbank durchsetzt** (Migration `20260904140000`)
- RLS für alle neun Tabellen, wie in jeder Phase zuvor.
- Trigger `buchung_unveraenderlich` — festgeschriebene Buchungen lassen sich
weder ändern noch löschen. Änderbar bleibt genau eines: der Verweis auf die
Gegenbuchung. Eine Stornierung entsteht später; sie einzutragen ist keine
Änderung des Vorgangs, sondern seine Fortschreibung.
- Trigger `buchung_zeitraum_offen` — ein geprüfter Zeitraum nimmt weder eine
neue Buchung an noch das Hineindatieren einer vorhandenen.
- Trigger `abschluss_unveraenderlich` — ein festgeschriebener Abschluss bleibt,
wie er ist. Nachtragen lässt sich nur der Prüfbericht, denn das PDF entsteht
im Hintergrund und damit nach dem Festschreiben.
- `REVOKE DELETE ON kassenabschluss` für die Anwendung.
- CHECKs, die verhindern, was sich hinterher nicht mehr erkennen ließe: ein
Betrag von null oder darunter, eine Buchung an eine Person **und** einen
Dritten zugleich, ein erlassener Beitrag ohne Begründung, ein Abschluss mit
einer Differenz, die nicht zu seinen eigenen Zahlen passt, eine Rechnung,
die fällig wird, bevor sie geschrieben wurde.
**Kassenbuch**
- Buchungsmaske mit **Kategorievorschlag**: zuerst aus den eigenen früheren
Buchungen der Wehr, erst danach aus einer kurzen Stichwortliste. Unter der
Hälfte gemeinsamer Wörter schweigt der Vorschlag — ein falscher wird
übernommen, ohne gelesen zu werden.
- Summen über **alle** Treffer, nicht nur über die angezeigte Seite.
- Festschreiben als Sammelaktion bis zu einem Stichtag.
- Gegenbuchen mit Pflichtbegründung; das Buchungsdatum ist immer heute, damit
ein geprüfter Zeitraum zubleibt.
- **Aus einem gegengezeichneten Beleg wird eine Buchung.** Die Brücke zu
Phase 8: fotografiert und ausgefüllt wird am Terminal oder am Handy,
gebucht wird in der Kasse — mit Konto und Kategorie, die keine Maschine
entscheiden kann. Das Foto hängt danach an der Buchung, und genau das
verlangt eine Kassenprüfung: zu jeder Ausgabe ein Beleg, an der Buchung
und nicht in einem Ordner daneben. Ein Entwurf lässt sich nicht buchen.
**Kontoauszug einlesen**
- CAMT.053 und MT940, beides ohne zusätzliche Abhängigkeit. Erkannt wird am
Inhalt, nicht an der Dateiendung.
- Es entstehen **Entwürfe, keine fertigen Buchungen**.
- Jeder Umsatz trägt eine Kennung — die der Bank, sonst eine aus Datum, Betrag
und Zweck gebildete. Ein zweimal eingelesener Auszug wird dadurch als solcher
erkannt, statt einen Monat doppelt zu buchen.
- Was nicht gelesen werden konnte, steht mit Grund daneben, statt still zu
verschwinden.
**Beiträge**
- Sätze je Abteilung, gültig ab einem Jahr und nicht rückwirkend änderbar.
- Beitragslauf mit **Vorschau**: wie viele Beiträge entstehen, welche Summe —
und wer keinen passenden Satz hat. Der Lauf ist wiederholbar, ohne dass
etwas doppelt entsteht.
- Zahlungseingang erzeugt zugleich die Buchung. Ein Häkchen ohne Geld in der
Kasse wäre eine Behauptung.
- Mahnen als Sammelaktion, Erlassen nur mit Begründung.
- Rückstandsliste über alle Jahre — die Ansicht, in der auffällt, wer seit
drei Jahren offen steht.
**Rechnungen**
- Fortlaufende Nummer je Jahr, lückenlos: eine fehlende Nummer muss in der
Kassenprüfung erklärt werden.
- Der Betrag ist die Summe der Positionen, gerechnet und nicht eingegeben.
- Bis zum Versenden ein Entwurf, danach unveränderbar. Korrigiert wird über
Storno und eine neue Rechnung — die stornierte bleibt mit Grund stehen.
- PDF über Gotenberg, im Hintergrund erzeugt.
**Kassenprüfung und Auswertung**
- Prüfmaske: links der Bestand laut Buch, rechts der gezählte. In dieser
Reihenfolge — wer erst das Buch sieht und dann zählt, zählt, bis es passt.
- Ohne Prüfer und Prüfdatum lässt sich nichts festschreiben; eine Differenz
ohne Erläuterung ebenso wenig.
- Kassenprüfbericht als PDF mit vier Unterschriftsfeldern.
- Jahresauswertung nach Kategorie und Monat, mit Vorjahresvergleich.
- CSV-Export mit Semikolon und BOM — was Excel in Deutschland ohne Rückfrage
richtig öffnet.
**Tests**
- 67 neue Unit-Tests: Geldrechnen, beide Auszugsformate, Kategorievorschlag,
und dreizehn gegen eine echte Datenbank, die die Trigger und CHECKs prüfen.
Sie laufen über den Systemzugang: kommt nicht einmal der Eigentümer der
Datenbank daran vorbei, kommt niemand vorbei.
- 13 E2E-Tests über Desktop und Tablet.
**Seed**
- Ein vollständiges Kassenjahr: drei Konten, der Standardkontenrahmen,
24 Buchungen über zwei Jahre, die Mitgliedsbeiträge mit Rückständen, zwei
Rechnungen und die festgeschriebene Kassenprüfung des Vorjahres. Die Zahlen
des Abschlusses sind die Summe der Buchungen und nicht erfunden.
### Geändert
- Ein E2E-Test aus Phase 0 sicherte zu, dass die Kasse in der Navigation
**kein** Verweis ist — sie war ja noch nicht da. Die Zusicherung ist mit
dieser Phase abgelaufen und prüft jetzt das Gegenteil. Ein künftiges Modul
zum Ausgrauen gibt es nicht mehr; kommt später eines dazu, gehört sie
zurück.
### Behoben
- **Die Rechnungsnummer war ein Wettlauf.** Höchste lesen und dann schreiben:
schreiben zwei Leute gleichzeitig eine Rechnung, lesen beide dieselbe
höchste Nummer, und die zweite läuft in den Eindeutigkeitsfehler. Gefunden
hat das der parallele E2E-Lauf. Der seltene Zusammenstoß wird jetzt erkannt
und die nächste Nummer genommen — lückenlos bleibt sie dabei.
- **Eine Organisation ließ sich nicht mehr löschen**, sobald sie eine
festgeschriebene Buchung hatte: der Trigger lehnte auch das Löschen im
Aufbewahrungslauf ab. Er kennt jetzt denselben Schalter wie das Audit-Log
und das Prüfprotokoll (`app.audit_bereinigung`). Ohne ihn wäre die
DSGVO-Löschung einer ausscheidenden Wehr nicht durchführbar gewesen.
---
## [0.9.1] — 2026-09-04
Aufräumarbeiten vor Phase 9. Drei Punkte, die sich über mehrere Phasen
angesammelt hatten und in jeder Phase Zeit gekostet haben.
### Hinzugefügt
- **ESLint** hat endlich eine Konfiguration (`eslint.config.mjs`, Flat Config).