# Changelog Alle nennenswerten Änderungen an FLORIAN. Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionierung nach [Semantic Versioning](https://semver.org/lang/de/). Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md). ## [0.11.0] — 2026-09-05 Phase 10: Mandantenfähigkeit. Ein Konto, mehrere Wehren, eine Kreisebene, die zusieht — und eine Verwaltung für den, der die Installation betreibt. **Das Anmeldekonto ist von der Personenakte gelöst.** Bis hierher war die Akte zugleich das Konto: Passwort-Hash und OIDC-Subject hingen an `person`, und `person` gehört genau einer Wehr. Ein Mensch konnte damit nicht zu zwei Wehren gehören, ohne zwei Passwörter zu haben — und die Anmeldung per E-Mail war mehrdeutig, sobald dieselbe Adresse in zwei Wehren stand, weil die Eindeutigkeit nur je Organisation galt. **Das Konto trägt keine Mandantenkennung — und kann keine tragen.** Es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehört. Geschützt wird es deshalb wie `Berechtigung` aus Phase 0: über Rechte statt über Row Level Security. Was ein Konto in einer Wehr darf, entscheidet nicht es selbst, sondern seine `Mitgliedschaft` — und die trägt `organisationId` wie jede andere Tabelle. **Die Kreisebene sieht und ändert nichts.** Ihre Rechte werden nicht aufgezählt, sondern gefiltert: alles, was `.lesen` heißt, und sonst nichts. Eine Liste von Hand wäre bei der nächsten Phase veraltet, und zwar unbemerkt. ### Hinzugefügt **Datenmodell** - `Benutzer` — das Anmeldekonto. Global eindeutige E-Mail, ein Passwort, ein OIDC-Subject. Ohne `organisationId`, mit Begründung im Schema. - `Mitgliedschaft` — die Zugehörigkeit eines Kontos zu einer Wehr, als `MITGLIED` (mit Personenakte) oder `AUFSICHT` (ohne). - `Person` verliert die Anmeldefelder. Der Passwort-Hash kann damit aus einer Personenabfrage gar nicht mehr herausfallen — die drei Stellen, die ihn bisher vorsichtshalber abgeschnitten haben, sind entfallen. **Was die Datenbank durchsetzt** (Migrationen `20260905090000` und `20260905091500`) - RLS für `mitgliedschaft` wie für jede andere Tabelle. Eine Wehr sieht damit auch nicht, in welchen anderen ein Konto noch geführt wird. - Trigger `mitgliedschaft_akte_gleiche_wehr` — eine Zuordnung, deren Personenakte in einer anderen Wehr liegt, wird abgelehnt. Ein Fremdschlüssel prüft nur, *dass* es die Akte gibt, nicht *wo* sie liegt; ohne den Trigger ließe sich ein Konto über eine fremde Akte in eine Wehr hängen. - Trigger `benutzer_kennung_unveraenderlich` — E-Mail, OIDC-Subject und das Verwaltermerkmal lassen sich nicht aus einer Wehr heraus ändern. - `REVOKE INSERT, DELETE ON benutzer` für die Anwendungsrolle. - CHECKs: `MITGLIED` braucht eine Akte, `AUFSICHT` darf keine haben; ein Konto braucht einen Anmeldeweg; eine E-Mail ohne @ ist keine. **Die Umstellung ist datenerhaltend.** Jede Person mit Anmeldedaten bekommt ein Konto und eine Mitgliedschaft, bevor die alten Spalten fallen. Wer schon mit derselben Adresse in zwei Wehren stand, bekommt **ein** Konto mit **zwei** Mitgliedschaften — genau das, was vorher nicht möglich war. Eine Prüfung am Ende bricht die Migration ab, statt einen halben Zustand zu hinterlassen. **Oberfläche** - **Wehrwähler** in der Seitenleiste. Er erscheint nur bei mehr als einer Wehr — ein Auswahlfeld mit genau einem Eintrag ist eine Frage ohne Antwortmöglichkeit. - Nach der Anmeldung öffnet die **eigene** Wehr, nicht die betreute. - Ein dauerhafter Hinweis, wenn man eine Wehr als Kreisaufsicht sieht — nicht als kurz aufblitzende Meldung. - **`/wehren`** für den Verwalter: Wehren anlegen (samt Systemrollen), Konten führen, Zuordnungen vergeben und beenden. Jede Zuordnung wird in der betroffenen Wehr protokolliert, nicht beim Verwalter — sonst hätte der Betreiber einen Weg, sich still in fremde Daten zu setzen. **Rechte** - Neue Rolle `AUFSICHT` — bewusst **nicht** im Prisma-Enum: sie wird nicht vergeben, sondern ergibt sich aus der Mitgliedschaftsart. - Ausdrücklich ausgenommen, obwohl sie auf `.lesen` enden: `kasse.lesen` (die Kasse gehört dem Verein oder der Kommune, nicht der Kreisaufsicht), `person.export`, `system.auditlog`, `ki.protokoll`. - `istVerwalter` als Merkmal des Kontos, nicht als Recht. **Tests** - 19 neue Unit-Tests, davon 11 gegen eine echte Datenbank: Doppelmitgliedschaft, der Trigger gegen wehrfremde Akten, die CHECKs und die Trennung der Zuordnungen über RLS. - 13 E2E-Tests: kein Wähler bei einer Wehr, Wechsel in die betreute und zurück, die Mitglieder der anderen Wehr, die verschlossene Kasse, und dass der Administrator die Umstellung überlebt hat. **Seed** - Eine zweite, kleine Wehr je Mandant und ein Konto, das in der einen Mitglied und in der anderen Kreisaufsicht ist. Ohne sie ließe sich die Phase nicht ausprobieren — und eine Mandantentrennung, die nie zwei Mandanten hatte, ist keine geprüfte. ### Geändert - Eine Sitzung ohne Personenakte ist gültig. Bis hierher galt genau das als „nicht angemeldet"; für die Kreisaufsicht, die in der betreuten Wehr keine Akte hat, hätte jeder Aufruf auf der Anmeldeseite geendet. - `Sitzung.personId` ist nullbar. Der Typechecker hat damit die fünf Stellen gezeigt, an denen eine Aufsichtssitzung nichts verloren hat — Anmeldemarke, Dienstausweis, Prüfungswizard, Dateiupload, Atemschutz. - Die Mitgliederansicht zeigt statt eines ungenutzten Zeitstempels, ob jemand überhaupt ein Konto hat. Das ist die nützlichere Auskunft: ohne Konto kommt er nicht in FLORIAN hinein. ### Behoben - **Ein Testaufbau war nicht wiederholbar.** `ki-datenbank.test.ts` legte Wehr und Person idempotent an, die Wiki-Seite aber nicht — blieb aus einem abgebrochenen Lauf etwas stehen, scheiterte der ganze Datei-Lauf an einer Eindeutigkeit, die mit dem Geprüften nichts zu tun hat. - **`db:test-reset` ließ die Nachbarwehr stehen** und der Seed hielt daraufhin alles für erledigt — die Mitgliedschaft in der neu angelegten Testwehr entstand nie wieder. Der Reset nimmt sie jetzt mit, und der Seed prüft Nachbarwehr und Kreiskonto getrennt statt mit einem gemeinsamen Ausstieg. - **Der Fehlversuch-Test sperrte das gemeinsame Testkonto.** Fünf Fehlversuche sperren ein Konto für fünf Minuten; bei zwei Projekten und Wiederholungen kam das im vollen Lauf zusammen, und fremde Testdateien fielen mit „Anmeldung fehlgeschlagen“ aus. Er nimmt jetzt eine Adresse, die es nicht gibt — die Meldung muss ohnehin dieselbe sein. --- ## [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). Bis dahin gab es das Skript `npm run lint`, aber nichts, was es hätte prüfen können — ein Werkzeug, das aussieht, als prüfe es etwas, und nichts prüft, ist schlechter als gar keines. Bewusst **ohne** Formatierungsregeln: die macht Prettier. Dazu `npm run lint:fix`. - **Eine eigene Wehr für die E2E-Tests.** `npm run db:test-reset` legt `ff-teststelle` neu an — vollständig, mit denselben Beispieldaten, aber unter eigener Kennung. Ein Riegel im Skript bricht ab, wenn die Testkennung mit der der Beispielwehr übereinstimmt. - `tests/e2e/wehr.ts` — Zugangsdaten und Anmeldung an einer Stelle statt achtmal wortgleich in acht Dateien. Schlägt die Anmeldung fehl, sagt die Meldung, dass die Testwehr fehlt, statt zweihundertmal „Timeout". ### Behoben - **Die E2E-Tests schrieben in die Beispielwehr.** Nach fünf Phasen standen dort 120 Atemschutz-Überwachungen statt einer, 115 Geräte statt 45 und 39 Einsätze statt 8. Zweimal fielen Tests aus, weil ein gesuchter Eintrag von Seite 1 gerutscht war; bei jedem Lauf unterschieden sich alle 72 Bildschirmfotos. Der Bestand ist einmal zurückgesetzt, die Tests laufen jetzt gegen die eigene Wehr. - **`
` auf der Einsatzseite** — die Meldung stand seit
Phase 3 in der Konsole und ist der klassische Auslöser für einen
Hydration-Fehler. Die Wurzel war das `Badge`: es rendert jetzt ein ``
statt eines `