v1.6.0
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9fe6f10f25 |
Rueckkaufswerte aller Versicherungen in einem Zug anfragen
Vor Vermoegensverzeichnis und Jahresbericht braucht es von jeder Lebens- und Rentenversicherung den Rueckkaufswert zum Stichtag - jedes Mal dieselben fuenf bis zehn Briefe. Neu unter Vertraege: Stichtag waehlen, Liste pruefen, ein Klick. Heraus kommt ein ZIP mit einer Word-Datei je Gesellschaft plus Begleitblatt; je Vertrag entstehen nebenbei die PDF-Fassung in der Akte, die Wiedervorlage nach 28 Tagen und der Anfragevermerk. Aufbau: - Auswahllogik als reine Funktionen in vertraege/serienabfrage.ts: Vorauswahl, Hindernisse mit Begruendung statt stummem Ausblenden, 28-Tage-Sperre im Gleichschritt mit der Wiedervorlage. - Der Lauf in vertraege/serienlauf.ts: je Vertrag das volle Paket aus Issue #4, aber in einem Durchgang. Kein Alles-oder-nichts - ein gescheiterter Vertrag steht mit Grund im Begleitblatt, die uebrigen laufen durch. Eine Transaktion ueber alle waere die falsche Zusage. - Dublettenschutz tagesgenau: Der Doppelklick erzeugt nichts Doppeltes, die bewusste zweite Anfrage nach einer Woche bleibt moeglich. - Die Anzahl steht vor dem Klick auf dem Knopf. Gekuendigte Vertraege bleiben anfragbar: Bis zum Wirksamwerden besteht der Vertrag, und genau dann will der Sozialhilfetraeger den Wert sehen. Geprueft: zwoelf Einheitstests, drei End-to-End-Tests inklusive der Nebenwirkungen in der Datenbank und des Dublettenschutzes. 602 Unit-Tests, 87 e2e, null Typfehler. Refs #6 |
||
|
|
b59d11c9b4 |
Schreiben als E-Mail uebernehmen
Im Schreiben-Editor gibt es jetzt den Umschalter Brief/E-Mail. Die E-Mail-Fassung wird aus dem - womoeglich bearbeiteten - Brieftext abgeleitet: ohne Absenderblock, Anschrift und Datumszeile, aber mit dem Bestellungsabsatz. Betreff und Text einzeln kopierbar. Die offene Entscheidung aus dem Issue (mailto oder .eml) ist nach Rueckfrage mit BEIDES beantwortet: - mailto fuer kurze Texte. Der Knopf verschwindet, sobald der kodierte Link 1800 Zeichen uebersteigt - mailto uebergibt den Text in der Adresszeile, deren Laenge nirgends zugesichert ist, und ein halbierter Brief faellt erst beim Empfaenger auf. Die Grenze wird NACH der URL-Kodierung gerechnet; 900 Umlaute sind roh harmlos und kodiert weit drueber. - .eml immer: RFC 5322 mit X-Unsent: 1, quoted-printable fuer den Rumpf, RFC-2047-Kodierung fuer Umlaute im Betreff. Bewusst ohne From-Kopf - ein erfundener Absender wuerde je nach Programm uebernommen. Beide Knoepfe erscheinen nur mit E-Mail-Adresse (aus dem Kontakt uebernommen oder von Hand). Ohne Adresse bleibt das Kopieren. Die Ablage kennt den Unterschied: Eine E-Mail heisst in der Akte "E-Mail: ...", und abgelegt wird die E-Mail-Fassung samt Empfaengeradresse im Kopf der PDF - in der Akte soll stehen, was tatsaechlich hinausging. Sicherung in emailFassung(): Findet sich keine Anrede (frei umgebauter Text), wird NICHTS entfernt. Lieber ein Briefkopf zu viel in der Mail als stillschweigend verlorener Inhalt. Geprueft: 20 Einheitstests, vier End-to-End-Tests auf beiden Geraeteprofilen. 590 Unit-Tests, 81 e2e, null Typfehler. Refs #5 |
||
|
|
c21d03abf6 |
Schreiben erzeugen, ausgeben und ablegen
Die Anschreiben gab es bisher nur als Text zum Kopieren. Danach war
Handarbeit: in Word nachbauen, ausdrucken, abschicken - und noch einmal
von Hand als Dokument in die Akte legen. Der letzte Schritt wurde am
haeufigsten vergessen, und dann fehlt der Nachweis im Bericht.
Neu unter /schreiben/neu: bearbeiten, als Word-Datei herunterladen, in
die Akte legen, Wiedervorlage setzen - in einem Zug. Eine Seite fuer alle
Arten und alle drei Empfaengerwege, statt die Bearbeitung dreimal an
Vertrag, Vorgang und Freitext nachzubauen.
Der dritte Weg ist der eigentliche Zugewinn: Schreiben an eine Stelle,
zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung ist das
der Normalfall - man fragt ja an, um zu erfahren, ob dort ueberhaupt
etwas besteht. Aus der freien Anschrift laesst sich mit einem Klick ein
Kontakt anlegen.
Zwei Entscheidungen, die erklaert gehoeren:
Die Akte bekommt ein PDF, heruntergeladen wird .docx. Der
Objektspeicher nimmt nur PDF und Bilder an, geprueft an den Magic Bytes.
Eine .docx ist ein ZIP-Archiv; sie durchzulassen hiesse jedes ZIP
durchzulassen, und diese Pruefung ist eine bewusste Zusage der
Anwendung. Sachlich ist PDF fuer den Nachweis ohnehin richtiger: Es
aendert sich nicht mehr. Die Oberflaeche sagt dazu, dass eine spaeter in
Word geaenderte Fassung von der Akte abweicht.
Der Berichtssatz in berichte/docx.ts bleibt unangetastet. Er setzt einen
Bericht mit Kopfzeilen und Unterschriftenblock; ein Brief braucht
Absender, Anschriftenfeld, Datumszeile und Grussformel. Beides in eine
Funktion zu zwingen haette beide Ausgaben schlechter gemacht. Der
Umsetzungshinweis im Issue ("muss nur angebunden werden") war insoweit
falsch - von mir selbst geschrieben.
Die vorhandenen Erzeuger (Rueckkaufswert, Kuendigung, Sachstandsanfrage)
bleiben unveraendert; ihr Wortlaut ist getestet und erprobt.
Geprueft: zehn Einheitstests, darunter der Gleichschritt der
Wiedervorlagefristen ueber zwei Module hinweg. Fuenf End-to-End-Tests
auf beiden Geraeteprofilen: Entwurf, Vertragsbezug, Word-Download,
Ablage samt Wiedervorlage, Kontakt aus freier Anschrift.
Refs #4
|
||
|
|
fa994350d6 |
Dauerzahlungen zu einem Vertrag gelten als belegt
Bei einer monatlichen Praemie gibt es keinen Beleg - es gibt einen Vertrag, und der wird einmal abgeschlossen, nicht monatlich quittiert. Bisher blieb nur ein Freitextfeld pro Buchung: zwoelfmal im Jahr derselbe Satz, mal Anzahl der Vertraege. Buchung.vertragId mit onDelete: SetNull. Der Belegzustand kennt jetzt drei Faelle statt zwei, getrennt ausgewiesen - ein Vertrag belegt, eine Begruendung erklaert nur; zusammengeworfen verliert die Belegquote ihre Aussage gegenueber dem Rechtspfleger. Dazu: automatische Zuordnung beim Import bei eindeutiger Gegenstelle, Sammelzuordnung ueber die Gruppen aus Issue #3, und die Rechnungslegung nennt den Vertrag samt Nummer statt "ohne Beleg". Die Sicherung dagegen: Weicht der Betrag um mehr als ein Fuenftel vom hinterlegten Beitrag ab, faellt die Anerkennung weg und die Buchung wird als "Betrag pruefen" markiert. Sonst waere die Zuordnung eine Hintertuer. Zur Toleranz - ein Entwurf und seine Korrektur: Zuerst verglich ich den Betrag gegen alle ueblichen Zahlweisen, damit es auch passt, wenn die Bank anders abbucht als der Vertrag es fuehrt. Der eigene Test hat das widerlegt: Bei 42,50 monatlich liegen 420 innerhalb von 20 Prozent des Jahresbetrags von 510 - ausgerechnet der Fall aus der Aufgabenstellung waere durchgerutscht. Jetzt wird stur gegen den hinterlegten Beitrag verglichen. Bucht die Bank anders ab, stimmen die Vertragsdaten nicht, und das soll auffallen. Behoben: Die Buchungsliste hatte eine eigene, fast gleiche Datenabfrage neben der aus berichte/gemeinsam.ts. Nur eine von beiden wurde nachgezogen, in der Liste stand weiter "Beleg fehlt". Beide benutzen jetzt BUCHUNG_EINBEZUG und zuBuchungEingabe - das kann nicht halb vergessen werden. Gefunden hat es der Browsertest. Nebenbei: Die Roadmap trug noch "Aktuelle Fassung 1.1.2" - Nachwirkung der parallelen Bearbeitung von #3 und #10. Nachgezogen wie zugesagt. Geprueft: 23 neue Einheitstests, die Haelfte davon darauf gerichtet, dass die Anerkennung auch wirklich wegfaellt. Fuenf End-to-End-Tests auf beiden Geraeteprofilen. 560 Unit-Tests, 63 e2e, null Typfehler. Refs #11 |
||
|
|
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 |
||
|
|
0128bf71e7 |
Seitenleiste bleibt stehen, Benutzermenue wieder erreichbar
Am Schreibtisch war "Mein Konto" praktisch nicht zu finden - und damit auch nicht Passwortwechsel, zweiter Faktor und Passkey-Verwaltung. Die Seitenleiste hatte keine feste Hoehe und wuchs mit der Seite mit. Ihr Fuss mit dem Benutzermenue landete deshalb am Ende der SEITE statt am unteren Bildschirmrand: auf /heute bei 1632 Pixel in einem 900 Pixel hohen Fenster. Nachgemessen, nicht geraten - im Browser auf fuenf Seiten. Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt, die Seite sah richtig aus. Man musste nur ganz nach unten scrollen. Behoben mit lg:sticky, lg:top-0, lg:h-dvh und lg:self-start. Das self-start gehoert dazu, weil ein Flex-Kind sonst auf die volle Zeilenhoehe gestreckt wird und die feste Hoehe wieder verliert. Die Navigation dazwischen hat bereits overflow-y-auto und scrollt bei Bedarf in sich selbst. Am Handy war es nie ein Problem - dort sitzt das Menue in der ohnehin festen Kopfzeile. Neu: e2e/benutzermenue.spec.ts fragt nicht, ob das Menue im DOM ist, sondern ob es im sichtbaren Bereich steht - auf fuenf Seiten, an drei Scrollpositionen, auf einem flachen Laptopfenster und am Handy. Die Position in der Mitte ist Absicht: Am Seitenende wird eine mitwachsende Leiste zufaellig doch sichtbar, der Test waere gruen und der Fehler trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand gegengeprueft und schlagen dort an. Refs #1 |
||
|
|
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. |
||
|
|
dccc34d669 |
test: Abnahmekriterien als Playwright-Tests, Anmeldungen im Protokoll
Zehn Tests, die genau die Zusagen aus dem Auftrag pruefen - an der laufenden Anwendung, nicht an einzelnen Funktionen: - Aus dem Bestellungsdatum entstehen die gesetzlichen Fristen, mit Rechtsgrundlage, und die Aufwandspauschale ist als Ausschlussfrist gekennzeichnet. - Ein PDF laesst sich hochladen und in zwei Klicks einem Vorgang zuordnen. - Eine Datei, die sich nur als PDF ausgibt, wird abgewiesen. Das ist der Angriff, gegen den die Pruefung an den Magic Bytes gebaut ist: HTML mit PDF-Endung und PDF-MIME-Typ, das spaeter als Seite ausgeliefert wuerde. - Ein genehmigungspflichtiger Vorgang bietet die Stufe "Vollzogen" gar nicht erst an, solange kein Beschluss hinterlegt ist (§ 1858 BGB). - Das Vermoegensverzeichnis kommt als echtes PDF heraus - geprueft wird die Signatur %PDF-, nicht nur der Dateiname. - Der Vollexport verlangt eine frische Anmeldung. - Am Handy: kein waagerechtes Scrollen der Seite, Bedienelemente mindestens 40 Pixel hoch, Schublade per Escape schliessbar. Dazu ein echter Fund aus dem Protokolltest: Anmeldungen und Abmeldungen tragen keine betreuteId - sie haengen am Konto, nicht an der Akte. Die Protokollansicht filterte nur nach betreuteId und liess damit ausgerechnet die Frage weg, wer wann auf die Akte zugegriffen hat. Jetzt werden beide Arten von Eintraegen gezeigt. 34 Tests, je einmal am Schreibtisch und am Handy. Etappe: 8 Phase: Haertung |
||
|
|
1a91f62ced |
feat: Erinnerungen, Wartungslauf und Playwright-Tests der Anmeldung
Etappe 8, erster Teil. Erinnerungsmails sind bewusst eine Zugabe: Ist kein SMTP konfiguriert, wird nichts gesendet und nichts geworfen. Die Fristen stehen ohnehin im Dashboard und im abonnierten Kalender - eine ausgefallene Mail darf nicht dazu fuehren, dass der Wartungslauf abbricht und danach auch das Aufraeumen liegen bleibt. In der Mail steht, DASS etwas faellig ist und wo man nachsieht - nicht, worum es geht. Kein Name der betreuten Person, keine Inhalte. E-Mail ist unterwegs unverschluesselt und landet auf fremden Servern. Je Frist geht hoechstens eine Mail pro Vorwarnstufe raus. Taeglich waere nach einer Woche Rauschen, das man wegklickt - und genau dann verpasst man die eine, auf die es ankam. Der Wartungslauf faengt jeden Schritt einzeln ab: Papierkorb leeren, Anmeldeversuche und altes Audit-Log aufraeumen laufen auch dann, wenn der SMTP-Server nicht antwortet. Angestossen wird er ueber /api/wartung mit einem HMAC aus AUTH_SECRET - ohne gueltigen Schluessel antwortet die Route mit 404 statt 401. Ein echter Fund aus den Tests: Auth.js reicht den Fehlercode aus authorize() nicht an die Server Action zurueck. Bei gesperrtem Zugang sah man deshalb nur die allgemeine Meldung "stimmen nicht" und haette ratlos weiterprobiert, obwohl die Sperre bereits lief. Die Sperre wird jetzt zusaetzlich in der Server Action geprueft und benannt; die Pruefung in authorize() bleibt als eigentliche Sperre bestehen. Sechs Playwright-Tests der Anmeldung, darunter zwei, die Sicherheitszusagen nachweisen: dass zwei Fehlversuche mit unterschiedlicher Ursache dieselbe Meldung liefern (sonst liesse sich ablesen, welche Adressen existieren), und dass die Anmeldeseite keine einzige Anfrage an einen fremden Host stellt. Etappe: 8 Phase: Haertung |