# Changelog --- ## 2026-08-11 – Überlange Methoden zerlegt: FinTsFetchService::run(), Booking::create()/reverse(), Bank-/InvoiceController (#159) - Rein strukturelle Änderung, keine Verhaltensänderung: bestehende Codeblöcke wurden in benannte Methoden verschoben, Bedingungen, Fehlermeldungen und Ablaufreihenfolge blieben unverändert. - **`src/Services/FinTsFetchService.php`:** `run()` war mit 274 Zeilen der größte Ausreißer im Projekt und dabei von außen nicht testbar, weil sie eine echte Bankverbindung voraussetzt. Aufgeteilt in `validateConfig()`, `assertUrlSafe()`, `connect()`, `selectTan()` (mit `selectTanMode()`/`selectTanMedium()`), `login()`, `resolveAccounts()` (mit `fetchAccounts()`/`applyAccountFilter()`), `computeFetchRange()` sowie `fetchAllStatements()`/`fetchStatementsForAccount()`/`fetchAccountBalance()`/`importTransactions()`. `run()` liegt jetzt bei 50 Zeilen. Die doppelte SSRF-Prüfung aus Issue #130 (DNS-Rebinding-Fenster zwischen Validierung und Verbindungsaufbau) ist unverändert erhalten, der erklärende Kommentar sitzt weiterhin direkt am zweiten `assertUrlSafe()`-Aufruf in `run()`. - **`src/Models/Booking.php`:** `create()` (144 Zeilen) und `reverse()` (95 Zeilen) enthielten mehrere Schreibpfade im selben Transaktionsblock. Die tatsächlichen Pfade in `create()` sind Hauptbuchung, eine VSt-/USt-Korrekturzeile für normale Buchungen mit 7/19 % (nicht „Eigenanteil“, wie im Issue-Text vermutet – dieser Begriff bezeichnet im Projekt die 30-%-Bewirtungskürzung aus `upsertEigenanteil30()`, ein unabhängiger Pfad außerhalb von `create()`) sowie die ig.-Erwerb-19-%-ReverseCharge-Kindzeilen (kein „Split“ im Sinne von `bucheBewirtungssplit()`). Extrahiert nach `insertHauptbuchung()`, `insertVorsteuerUmsatzsteuerZeile()`, `insertIgErwerbReverseCharge()`; `reverse()` entsprechend nach `insertHauptstorno()`/`insertKindstornos()`. Die Transaktionsklammer bleibt jeweils in `create()`/`reverse()`. - **`src/Controllers/BankController.php`:** `direktStore()` (111 Zeilen) aufgeteilt in `storeCsvFormat()`, `resolveProviderId()`, `storeBankSource()`; `direktUpdate()` (70 Zeilen) in `buildUpdateData()`, `buildApiUpdateData()`, `buildFintsUpdateData()`. - **`src/Controllers/InvoiceController.php`:** `storeBooking()` (84 Zeilen, drei identische Render-Blöcke für Validierungs-/PDO-/allgemeine Fehler) aufgeteilt in `validateBookingInput()`, `renderBookForm()` (bündelt die vormals dreifach kopierte View-Rendering-Logik), `persistBooking()`. - **`tests/88_FinTsHilfsmethodenTest.php`** (neu): `validateConfig()` gegen fehlende BLZ/Benutzerkennung/PIN/URL, `applyAccountFilter()` mit leerer/passender/nicht passender Filterliste, `accountId()` für Konten mit/ohne IBAN und mit/ohne Unterkonto – alles ohne echte Bankverbindung über Reflection auf die privaten Methoden. - **`tests/89_BookingCreatePfadeTest.php`** (neu): Deckt die drei Schreibpfade von `Booking::create()` ab – reine Hauptbuchung, Hauptbuchung mit VSt-Korrekturzeile (19 %), ig.-Erwerb-19-%-Buchung mit zwei Kindzeilen. ## 2026-08-11 – Duplizierte View-Logik gebündelt: Kontengruppierung, Kontrastfarbe, Helper-Boilerplate (#158) - **Kontengruppierung** (`invoices/book.php`, `invoices/show.php`, `belege/_buchungsform.php`) war dreimal kopiert und bereits divergiert: Nur `belege/_buchungsform.php` (per `include` in `detail.php`) filterte Zahlungskonten auf den Nummernbereich 1200–1299, die beiden Rechnungs-Views nicht. Neue Methode `View::gruppiereKonten(array $accounts, bool $nurZahlungskonten1200 = false)` bündelt beide Varianten; alle drei Views wenden jetzt den Filter an (Parameter `true`). - Entscheidung für „Filter überall anwenden“ statt „nirgends“ oder „Unterschied beibehalten“: `BuchungsvorlageController::modal()` filtert unabhängig davon bereits identisch auf 1200–1299 – das ist die etablierte Konvention für „welches Bankkonto hat die Zahlung erhalten“-Auswahlen im Projekt, nicht nur ein Sonderfall der Belegerfassung. Produktiv gab es zum Zeitpunkt der Änderung noch keine einzige Rechnungs-Zahlungsbuchung (0 Zeilen in `bookings` mit verknüpfter `invoices`), das Risiko eines bestehenden Workflows, der auf Kasse/Verrechnungskonto/USt-Konten als Zahlungskonto einer Rechnung angewiesen ist, bestand also nicht. - Sichtbare Auswirkung: Im Zahlungs-Modal der Rechnungsdetailansicht und in `invoices/book.php` stehen jetzt nur noch die fünf Bankkonten (1200–1240) zur Auswahl, nicht mehr Kasse, Verrechnungskonto oder die Umsatzsteuerkonten. Siehe `docs/funktionen.md`. - **Kontrastfarbe**: Die Formel zur Wahl einer lesbaren Textfarbe (`0.299*hexdec(...)+0.587*hexdec(...)+0.114*hexdec(...)`) stand identisch in fünf Views (`belege/detail.php`, `bank/index.php`, `bank/show.php`, `bank/fints.php`, `settings/bankimport.php`). Neue Methode `View::kontrastfarbe(string $hex, string $dunkel = '#1e293b', string $hell = '#ffffff')` validiert die Eingabe (fehlt `#`, ist der Wert zu kurz oder kein gültiges Hex, liefert sie die Standardfarbe statt der bisherigen stillen Rechnung auf 0, die bei leerem/kurzem Wert immer weiße Schrift ergab); dreistellige Kurzform (`#abc`) wird expandiert. Alle fünf Views delegieren jetzt an `View::kontrastfarbe()`. - **Helper-Boilerplate**: `$h = fn($v) => View::h($v);` (36 Views) und `$fmt = fn($v) => View::formatMoney($v);` (8 exakte Duplikate; abweichende `$fmt`-Definitionen mit `abs()` bzw. ohne geschütztes Leerzeichen blieben unverändert) sind entfernt. `BaseController::renderView()` und `View::render()` stellen `$h`/`$fmt` jetzt per `$data['h'] ??= …` zentral bereit, bevor `extract()` läuft – Views, die die Variablen weiterhin selbst definieren, bleiben dadurch unverändert lauffähig. - **`tests/87_ViewHelperTest.php`** (neu): Deckt `View::kontrastfarbe()` (bekannte Farben, ungültige/leere Werte → Standardfarbe, Kurzform-Expansion, eigene Standardfarben) und `View::gruppiereKonten()` (mit/ohne Filter) ab. ## 2026-08-11 – belege/detail.php in Teil-Views und externe JS-Dateien aufgeteilt (#157) - `src/Views/belege/detail.php` war mit 1.548 Zeilen die mit Abstand größte Datei der Anwendung, davon 827 Zeilen JavaScript in neun `