3 Commits
Author SHA1 Message Date
Florian Wilde aa0cc66292 Wiederkehrende Zahlungen aufspueren
Am Anfang einer Betreuung weiss niemand, welche Vertraege es gibt. Die
Antwort steht aber im Kontoauszug: Wer regelmaessig abbucht, hat einen
Vertrag. Neu unter Vermoegen & Kasse wird aus hunderten Buchungszeilen
eine Arbeitsliste.

Aufbau:
- Die Erkennung liegt als reine Funktionen in
  src/lib/finanzen/wiederkehrend.ts, ohne Datenbank. Nur so laesst sich
  jede Einordnung einzeln nachrechnen.
- Der Datenzugriff steht getrennt in wiederkehrend-laden.ts.
- Ein kleines Modell haelt fest, was bewusst KEIN Vertrag ist. Es haengt
  am normalisierten Empfaengernamen, nicht an Buchungen - nur so
  ueberlebt die Entscheidung den naechsten Import.

Zwei Haltungen liegen der Erkennung zugrunde:

Im Zweifel getrennt lassen. Zusammengefasst wird nur das Unstrittige:
Gross- und Kleinschreibung, Satzzeichen, Umlaute, Rechtsform am Ende.
Keine Aehnlichkeitssuche - "Stadtwerke Bamberg" und "Stadtwerke
Bayreuth" trennen drei Zeichen und zwei Rechnungssteller. Zwei Zeilen zu
viel sind harmlos; eine falsche Gruppe verbirgt einen Vertrag.

Nichts behaupten, was nicht belegt ist. Statt "monatlich" steht dort
"6 Zahlungen, etwa monatlich". Schwankt der Betrag, wird die Spanne
genannt. Ohne erkennbaren Abstand wird nicht hochgerechnet, sondern
gezeigt, was geflossen ist.

Dazu eine zweite Liste fuer einzelne groessere Zahlungen: Die
Gruppierung braucht zwei Zahlungen, ein Jahresbeitrag erscheint bei
einem importierten Jahr aber nur einmal - und ausgerechnet
Versicherungen zahlt man oft jaehrlich.

Geprueft: 37 Einheitstests, die Haelfte davon in der Gegenrichtung, plus
vier End-to-End-Tests auf beiden Geraeteprofilen. Der Browsertest hat
zwei Fehler gefunden, die Typpruefung und Einheitstests durchgelassen
haben - darunter ein verlorener Backslash, der die Vorbelegung des
Beitrags stillschweigend verschluckte.

Nebenbei: .claude/ ins .gitignore. Dort legen parallel arbeitende
Agenten ihre Worktrees ab; ein "git add -A" haette einen kompletten
zweiten Checkout eingecheckt.

Refs #3
2026-08-23 02:42:26 +02:00
Florian Wilde c38c988c3c feat: Anmelden mit Passkey
Passkeys (WebAuthn/FIDO2) als zweiter Anmeldeweg - auf der Anmeldeseite
und bei der erneuten Bestaetigung vor sensiblen Aktionen. Verwaltet unter
"Mein Konto".

Der Passkey ERSETZT den zweiten Faktor, statt zu ihm hinzuzukommen. Das
ist tragbar, weil er zwei Faktoren in sich vereint: Besitz des Geraets
und dessen Entsperrung. Damit das keine Annahme bleibt, wird die
Nutzerverifikation angefordert (userVerification: 'required') und
serverseitig nachgeprueft - eine Antwort ohne sie wird abgewiesen.

TOTP muss vorher eingerichtet sein. Ein Passkey haengt an einem Geraet;
waere er der einzige Weg hinein, koennte ein verlorenes Handy die Akte
schliessen.

Aufbau:
- Die Pruefung laeuft vollstaendig im Credentials-Provider "passkey",
  nicht in der Server Action davor. Ein "war schon geprueft" ueber die
  Grenze zu reichen waere die Stelle, an der sich Anmeldungen faelschen
  lassen.
- Challenges liegen serverseitig und werden per DELETE genau einmal
  verbraucht - die Datenbank entscheidet, wer sie bekommt.
- Bereichskennung und Herkunft kommen aus AUTH_URL, nicht aus einer
  zweiten Konfigurationsquelle.
- Ohne https wird der Knopf gar nicht erst angeboten.
- Rate-Limiting, Kontosperre und Audit-Log gelten unveraendert; die
  Anmeldung nennt den verwendeten Passkey namentlich.

Zwei Anmerkungen zur Migration:
- "prisma migrate diff" wollte zusaetzlich die Trigramm-Indizes aus
  prisma/sql/suche.sql loeschen, die Prisma nicht kennt. Die Zeilen sind
  entfernt und im Migrationskopf vermerkt - ein DROP haette die unscharfe
  Suche stillgelegt, ohne dass etwas fehlgeschlagen waere.
- "prisma format" hat das Schema durchgaengig neu ausgerichtet. Ohne
  Whitespace betrachtet sind es 88 hinzugefuegte Zeilen und keine
  geloeschte.

Geprueft: 21 Einheitstests, drei End-to-End-Tests gegen Chromiums
virtuellen Authenticator (einrichten, anmelden, bestaetigen, entfernen,
Geraet ohne Verifikation abgewiesen), beide Geraeteprofile. 496 Tests,
null Typfehler, Produktionsbau steht.

Nebenbei: Die Zahlen im README waren veraltet (24 statt 33 Modelle, 46
statt 79 Seiten). Sie stehen jetzt mit dem Befehl daneben, mit dem man
sie nachzaehlt.
2026-08-22 18:44:56 +02:00
Florian Wilde 6c1915ab9c feat: Vollstaendiges Datenmodell als Prisma-Schema
Alle Fachentitaeten der Betreuung in einem Schema - bewusst komplett,
damit spaetere Etappen keine Migration am Fundament brauchen.

Kernentscheidungen:
- Geldbetraege durchgaengig als BigInt in Cent. Eine Rechnungslegung nach
  § 1865 BGB darf keine Rundungsfehler erben.
- Felder mit Suffix Verschluesselt tragen einen AES-256-GCM-Umschlag.
  Wo trotzdem gesucht werden muss (IBAN), steht ein HMAC-Blindindex daneben.
- Fristen und Stichtage sind @db.Date, nicht DateTime - sonst verschiebt
  die Zeitzone eine Frist um einen Tag.
- Frist.systemSchluessel macht die aus dem Bestellungsdatum abgeleiteten
  Pflichtfristen idempotent.
- Genehmigung.beschlussDokumentId ist die technische Voraussetzung dafuer,
  dass ein Vorgang ohne Beschluss nicht auf VOLLZOGEN gesetzt werden kann.
- DokumentVerknuepfung mit echten Fremdschluesseln statt loser Typ/ID-
  Spalte: eine Akte muss referenzsicher sein.
- Wertstand und Saldostand sind Historien - Werte werden nie ueberschrieben.

Dazu zwei SQL-Skripte, die Prisma nicht abbilden kann:
- volltextsuche.sql: tsvector-Trigger (deutsch) mit Gewichtung A/B/C,
  plus Trigram-Indizes fuer unscharfe Absendersuche.
- audit-schutz.sql: Trigger, die UPDATE und DELETE auf audit_log
  verweigern. Das Log ist Nachweis gegenueber Gericht und Miterben,
  also nur anhaengend - auch gegen psql.

Etappe: 1
Phase: Fundament
2026-08-22 01:36:36 +02:00