Phase 9: Kassenverwaltung #14

Merged
florian.wilde merged 1 commits from feat/phase-9-kassenverwaltung into main 2026-09-05 02:30:53 +00:00
Owner

Kassenbuch, Mitgliedsbeiträge, Rechnungen an Dritte, Kassenprüfung und die Auswertungen für die Jahreshauptversammlung. Version 0.10.0.

Der Kern

Eine Feuerwehrkasse wird von einem Ehrenamtlichen geführt und von zwei anderen geprüft. Was die Prüfung wert ist, hängt einzig daran, dass sich das Geprüfte hinterher nicht mehr ändern lässt. Deshalb ist der zentrale Punkt dieser Phase kein Formular, sondern eine Sperre — und die sitzt in der Datenbank, nicht im Router.

Trigger Was er verhindert
buchung_unveraenderlich Ändern oder Löschen einer festgeschriebenen Buchung
buchung_zeitraum_offen Neue Buchung in einem geprüften Zeitraum — und das Hineindatieren einer vorhandenen
abschluss_unveraenderlich Nachträgliche Änderung an Zahlen, Prüfern oder Erläuterung eines festgeschriebenen Abschlusses

Jeder hat genau eine Ausnahme, und jede hat einen Grund: die Buchung lässt den Verweis auf ihre Gegenbuchung eintragen (eine Stornierung entsteht später), der Abschluss lässt den Prüfbericht nachtragen (das PDF entsteht im Hintergrund). Ohne die zweite gäbe es nie einen Bericht.

Dazu zwölf 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.

Gerechnet wird durchgängig mit Prisma.Decimal. 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.

Was dazugekommen ist

  • Kassenbuch mit Kategorievorschlag aus den eigenen früheren Buchungen (erst danach aus einer Stichwortliste), Summen über alle Treffer statt über die angezeigte Seite, Festschreiben als Sammelaktion, Gegenbuchen mit Pflichtbegründung.
  • Kontoauszug einlesen, CAMT.053 und MT940, ohne Fremdbibliothek. Es entstehen Entwürfe, keine fertigen Buchungen. Jeder Umsatz trägt eine Kennung — ein zweimal eingelesener Auszug wird als solcher erkannt, statt einen Monat doppelt zu buchen.
  • Beiträge mit Sätzen je Abteilung, Beitragslauf samt Vorschau (inklusive: wer keinen passenden Satz hat), Zahlungseingang, der zugleich die Buchung erzeugt, Mahnstufen und einer Rückstandsliste über alle Jahre.
  • Rechnungen mit lückenlosem Nummernkreis je Jahr, Betrag aus den Positionen gerechnet, PDF über Gotenberg, Storno statt Löschen.
  • Kassenprüfung: links der Buchbestand, rechts leere Felder für das Gezählte — wer erst das Buch sieht und dann zählt, zählt, bis es passt. Prüfbericht als PDF mit vier Unterschriftsfeldern.
  • Auswertung nach Kategorie und Monat mit Vorjahresvergleich, CSV-Export mit Semikolon und BOM.
  • Die Brücke von Phase 8: aus einem gegengezeichneten Beleg wird eine Buchung. Konto und Kategorie wählen, fertig — das Foto hängt danach an der Buchung. Der buchungId stand seit Phase 8 im Schema, aber es gab keinen Weg dorthin.

Nebenbei behoben

  • Die Rechnungsnummer war ein Wettlauf. Höchste lesen, dann schreiben: zwei gleichzeitige Rechnungen lesen dieselbe Nummer, und die zweite läuft in den Eindeutigkeitsfehler. Gefunden hat es der parallele E2E-Lauf.
  • Eine Organisation ließ sich nicht mehr löschen, sobald sie eine festgeschriebene Buchung hatte — der DELETE-Trigger kannte den Aufbewahrungsschalter nicht, den Audit-Log und Prüfprotokoll haben. Das hätte die DSGVO-Löschung einer ausscheidenden Wehr blockiert.

Nachweise

  • npm run typecheck — ohne Ausgabe
  • npm run lint — ohne Befund
  • npm run test — 562 Tests grün, davon 67 neu (13 gegen eine echte Datenbank)
  • npm run test:e2e — 239 Tests grün über Desktop und Tablet, davon 26 neu
  • npm run build — fünf neue Routen unter /kasse
  • docker compose ps — Postgres, Redis, MinIO, Gotenberg gesund
  • Rechnungs-PDF und Kassenprüfbericht einmal von Hand über Gotenberg erzeugt und in MinIO abgelegt

Zum Testen

Nach npm run db:seed steht ein vollständiges Kassenjahr bereit: 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.

Sinnvolle Handgriffe:

  1. /kasse — Bestände, die aus den Buchungen gerechnet sind.
  2. /kasse/buchungen — eine Buchung erfassen und den Kategorievorschlag ansehen. Dann bei einer Vorjahresbuchung auf Gegenbuchen gehen: es gibt kein Bearbeiten.
  3. Beim Erfassen ein Datum aus dem Vorjahr eintragen — die Datenbank lehnt ab, mit Begründung.
  4. /kasse/beitraege — Beitragslauf öffnen. Die Vorschau sagt, dass nichts doppelt entsteht.
  5. /kasse/rechnungen — eine Rechnung schreiben, versenden, PDF herunterladen.
  6. /kasse/abschluss — Kassenprüfung anlegen mit dem Vorjahr: wird abgewiesen, der Zeitraum ist geprüft. Danach auf Auswertung wechseln.

Eine Anmerkung, weil sie eine Entscheidung von dir ist: die Mahnstufen enden bei der fünften (Beiträge) bzw. dritten (Rechnungen). Was danach kommt, entscheidet die Kommandantschaft — FLORIAN hört dort auf. Falls das bei euch anders gehandhabt wird, sag Bescheid.

🤖 Generated with Claude Code

Kassenbuch, Mitgliedsbeiträge, Rechnungen an Dritte, Kassenprüfung und die Auswertungen für die Jahreshauptversammlung. Version **0.10.0**. ## Der Kern Eine Feuerwehrkasse wird von einem Ehrenamtlichen geführt und von zwei anderen geprüft. Was die Prüfung wert ist, hängt einzig daran, dass sich das Geprüfte hinterher nicht mehr ändern lässt. Deshalb ist der zentrale Punkt dieser Phase kein Formular, sondern eine Sperre — und die sitzt in der Datenbank, nicht im Router. | Trigger | Was er verhindert | |---|---| | `buchung_unveraenderlich` | Ändern oder Löschen einer festgeschriebenen Buchung | | `buchung_zeitraum_offen` | Neue Buchung in einem geprüften Zeitraum — und das Hineindatieren einer vorhandenen | | `abschluss_unveraenderlich` | Nachträgliche Änderung an Zahlen, Prüfern oder Erläuterung eines festgeschriebenen Abschlusses | Jeder hat genau eine Ausnahme, und jede hat einen Grund: die Buchung lässt den Verweis auf ihre Gegenbuchung eintragen (eine Stornierung entsteht später), der Abschluss lässt den Prüfbericht nachtragen (das PDF entsteht im Hintergrund). Ohne die zweite gäbe es nie einen Bericht. Dazu zwölf 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. Gerechnet wird durchgängig mit `Prisma.Decimal`. `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. ## Was dazugekommen ist - **Kassenbuch** mit Kategorievorschlag aus den *eigenen* früheren Buchungen (erst danach aus einer Stichwortliste), Summen über alle Treffer statt über die angezeigte Seite, Festschreiben als Sammelaktion, Gegenbuchen mit Pflichtbegründung. - **Kontoauszug einlesen**, CAMT.053 und MT940, ohne Fremdbibliothek. Es entstehen Entwürfe, keine fertigen Buchungen. Jeder Umsatz trägt eine Kennung — ein zweimal eingelesener Auszug wird als solcher erkannt, statt einen Monat doppelt zu buchen. - **Beiträge** mit Sätzen je Abteilung, Beitragslauf samt Vorschau (inklusive: wer keinen passenden Satz hat), Zahlungseingang, der zugleich die Buchung erzeugt, Mahnstufen und einer Rückstandsliste über alle Jahre. - **Rechnungen** mit lückenlosem Nummernkreis je Jahr, Betrag aus den Positionen gerechnet, PDF über Gotenberg, Storno statt Löschen. - **Kassenprüfung**: links der Buchbestand, rechts leere Felder für das Gezählte — wer erst das Buch sieht und dann zählt, zählt, bis es passt. Prüfbericht als PDF mit vier Unterschriftsfeldern. - **Auswertung** nach Kategorie und Monat mit Vorjahresvergleich, CSV-Export mit Semikolon und BOM. - **Die Brücke von Phase 8**: aus einem gegengezeichneten Beleg wird eine Buchung. Konto und Kategorie wählen, fertig — das Foto hängt danach an der Buchung. Der `buchungId` stand seit Phase 8 im Schema, aber es gab keinen Weg dorthin. ## Nebenbei behoben - **Die Rechnungsnummer war ein Wettlauf.** Höchste lesen, dann schreiben: zwei gleichzeitige Rechnungen lesen dieselbe Nummer, und die zweite läuft in den Eindeutigkeitsfehler. Gefunden hat es der parallele E2E-Lauf. - **Eine Organisation ließ sich nicht mehr löschen**, sobald sie eine festgeschriebene Buchung hatte — der DELETE-Trigger kannte den Aufbewahrungsschalter nicht, den Audit-Log und Prüfprotokoll haben. Das hätte die DSGVO-Löschung einer ausscheidenden Wehr blockiert. ## Nachweise - `npm run typecheck` — ohne Ausgabe - `npm run lint` — ohne Befund - `npm run test` — **562 Tests grün**, davon 67 neu (13 gegen eine echte Datenbank) - `npm run test:e2e` — **239 Tests grün** über Desktop und Tablet, davon 26 neu - `npm run build` — fünf neue Routen unter `/kasse` - `docker compose ps` — Postgres, Redis, MinIO, Gotenberg gesund - Rechnungs-PDF und Kassenprüfbericht einmal von Hand über Gotenberg erzeugt und in MinIO abgelegt ## Zum Testen Nach `npm run db:seed` steht ein vollständiges Kassenjahr bereit: 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. Sinnvolle Handgriffe: 1. **`/kasse`** — Bestände, die aus den Buchungen gerechnet sind. 2. **`/kasse/buchungen`** — eine Buchung erfassen und den Kategorievorschlag ansehen. Dann bei einer Vorjahresbuchung auf *Gegenbuchen* gehen: es gibt kein *Bearbeiten*. 3. Beim Erfassen ein Datum aus dem Vorjahr eintragen — die Datenbank lehnt ab, mit Begründung. 4. **`/kasse/beitraege`** — *Beitragslauf* öffnen. Die Vorschau sagt, dass nichts doppelt entsteht. 5. **`/kasse/rechnungen`** — eine Rechnung schreiben, versenden, PDF herunterladen. 6. **`/kasse/abschluss`** — *Kassenprüfung anlegen* mit dem Vorjahr: wird abgewiesen, der Zeitraum ist geprüft. Danach auf *Auswertung* wechseln. Eine Anmerkung, weil sie eine Entscheidung von dir ist: die Mahnstufen enden bei der fünften (Beiträge) bzw. dritten (Rechnungen). Was danach kommt, entscheidet die Kommandantschaft — FLORIAN hört dort auf. Falls das bei euch anders gehandhabt wird, sag Bescheid. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
florian.wilde added 1 commit 2026-09-04 17:25:03 +00:00
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>
florian.wilde merged commit 2b53556175 into main 2026-09-05 02:30:53 +00:00
florian.wilde deleted branch feat/phase-9-kassenverwaltung 2026-09-05 02:30:53 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/FireAdministration#14