mandantAnlegen(); $this->stammdaten(); $this->einfacheBuchungen(); $this->umbuchungHatZweiSichten(); $this->splitbuchung(); $this->splitSummeWirdGeprueft(); $this->splitMitGegenkonto(); $this->salden(); $this->kontoblattFilter(); $this->pruefungen(); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Testgiro', 'kontoart' => 'bank', 'anfangssaldo' => 1000, 'aktiv' => 1, ]); $this->tagesgeld = Konto::anlegen([ 'name' => 'Testtagesgeld', 'kontoart' => 'sparkonto', 'anfangssaldo' => 5000, 'aktiv' => 1, ]); $this->kategorie = Kategorie::anlegen(['name' => 'Lebensmittel', 'art' => 'ausgabe']); $this->klasse = Klasse::anlegen('Privat'); } private function einfacheBuchungen(): void { $id = Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-03-18', 'betrag' => -49.90, 'empfaenger' => 'Supermarkt', 'buchungstext' => 'Wocheneinkauf', 'kategorie_id' => $this->kategorie, 'klasse_id' => $this->klasse, 'abgeglichen' => true, ]); $buchung = Buchung::find($id); $this->assertEquals('-49.90', $buchung['betrag'], 'Betrag wird übernommen'); $this->assertEquals('2026-03-18', $buchung['datum'], 'Datum wird übernommen'); $this->assertEquals(1, (int)$buchung['abgeglichen'], 'Abgleichstatus wird übernommen'); $this->assertEquals(0, (int)$buchung['gesplittet'], 'Ohne Splitzeilen keine Splitbuchung'); Buchung::speichern($id, [ 'konto_id' => $this->giro, 'datum' => '2026-03-19', 'betrag' => -59.90, 'empfaenger' => 'Supermarkt', 'kategorie_id' => $this->kategorie, ]); $this->assertEquals('-59.90', Buchung::find($id)['betrag'], 'Buchung lässt sich ändern'); Buchung::loeschen($id); $this->assertNull(Buchung::find($id), 'Buchung lässt sich löschen'); } private function umbuchungHatZweiSichten(): void { // 500 vom Giro aufs Tagesgeld. $id = Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-04-01', 'betrag' => -500.00, 'empfaenger' => 'Übertrag', 'gegenkonto_id' => $this->tagesgeld, ]); $this->assertEquals(1, Buchung::anzahl(), 'Die Umbuchung ist ein einziger Datensatz'); $giroBlatt = Buchung::kontoblatt($this->giro); $tagesBlatt = Buchung::kontoblatt($this->tagesgeld); $this->assertEquals(1, count($giroBlatt), 'Sie steht im Kontoblatt des Ausgangskontos'); $this->assertEquals(1, count($tagesBlatt), 'Und ebenso im Kontoblatt des Gegenkontos'); $this->assertEquals(-500.0, (float)$giroBlatt[0]['betrag'], 'Auf dem Ausgangskonto negativ'); $this->assertEquals(500.0, (float)$tagesBlatt[0]['betrag'], 'Auf dem Gegenkonto positiv'); $this->assert(!$giroBlatt[0]['gegensicht'], 'Auf dem Ausgangskonto ist es die eigene Sicht'); $this->assert($tagesBlatt[0]['gegensicht'], 'Auf dem Gegenkonto die Gegensicht'); $this->assertEquals(500.0, Konto::saldo($this->giro), 'Girosaldo: 1000 - 500'); $this->assertEquals(5500.0, Konto::saldo($this->tagesgeld), 'Tagesgeldsaldo: 5000 + 500'); Buchung::loeschen($id); $this->assertEquals(1000.0, Konto::saldo($this->giro), 'Nach dem Löschen ist der Saldo zurück'); $this->assertEquals(5000.0, Konto::saldo($this->tagesgeld), 'Auch auf dem Gegenkonto'); } private function splitbuchung(): void { $versicherung = Kategorie::anlegen(['name' => 'Versicherung', 'art' => 'ausgabe']); $id = Buchung::anlegen( [ 'konto_id' => $this->giro, 'datum' => '2026-01-02', 'betrag' => -260.49, 'empfaenger' => 'Versicherung AG', ], [ ['kategorie_id' => $versicherung, 'betrag' => -162.51, 'notiz' => 'Kasko'], ['kategorie_id' => $versicherung, 'betrag' => -97.98, 'notiz' => 'Haftpflicht'], ] ); $buchung = Buchung::find($id); $this->assertEquals(1, (int)$buchung['gesplittet'], 'Buchung ist als Splitbuchung gekennzeichnet'); $this->assertNull($buchung['kategorie_id'], 'Bei Splits steht oben keine Kategorie'); $splits = Buchung::splits($id); $this->assertEquals(2, count($splits), 'Zwei Splitzeilen'); $this->assertEquals('Kasko', $splits[0]['notiz'], 'Reihenfolge der Zeilen bleibt erhalten'); $this->assertEquals('-97.98', $splits[1]['betrag'], 'Betrag der zweiten Zeile'); // Beim Speichern werden die Zeilen ersetzt, nicht ergänzt. Buchung::speichern($id, [ 'konto_id' => $this->giro, 'datum' => '2026-01-02', 'betrag' => -260.49, ], [ ['kategorie_id' => $versicherung, 'betrag' => -260.49, 'notiz' => 'zusammengefasst'], ]); $this->assertEquals(1, count(Buchung::splits($id)), 'Splitzeilen werden ersetzt'); Buchung::loeschen($id); $this->assertEquals(0, count(Buchung::splits($id)), 'Mit der Buchung gehen die Splitzeilen'); } private function splitSummeWirdGeprueft(): void { $abgewiesen = false; try { Buchung::anlegen( ['konto_id' => $this->giro, 'datum' => '2026-02-01', 'betrag' => -100.00], [ ['kategorie_id' => $this->kategorie, 'betrag' => -60.00], ['kategorie_id' => $this->kategorie, 'betrag' => -30.00], ] ); } catch (\Throwable) { $abgewiesen = true; } $this->assert($abgewiesen, 'Splitzeilen, die den Betrag verfehlen, werden abgewiesen'); $this->assertEquals(0, Buchung::anzahl(), 'Es blieb nichts halb Gespeichertes zurück'); // Ein Cent Rundungsdifferenz aus der Betragsumrechnung ist zulässig. $id = Buchung::anlegen( ['konto_id' => $this->giro, 'datum' => '2026-02-01', 'betrag' => -100.00], [ ['kategorie_id' => $this->kategorie, 'betrag' => -66.67], ['kategorie_id' => $this->kategorie, 'betrag' => -33.33], ] ); $this->assertGreaterThan(0, $id, 'Aufgehende Splitzeilen werden angenommen'); Buchung::loeschen($id); } private function splitMitGegenkonto(): void { // Eine Zeile geht auf ein anderes Konto – das muss dort auftauchen. $id = Buchung::anlegen( ['konto_id' => $this->giro, 'datum' => '2026-05-02', 'betrag' => -300.00], [ ['kategorie_id' => $this->kategorie, 'betrag' => -100.00, 'notiz' => 'Einkauf'], ['gegenkonto_id' => $this->tagesgeld, 'betrag' => -200.00, 'notiz' => 'Rücklage'], ] ); $this->assertEquals(700.0, Konto::saldo($this->giro), 'Das Ausgangskonto verliert die volle Summe'); $this->assertEquals(5200.0, Konto::saldo($this->tagesgeld), 'Nur die eine Zeile landet auf dem Gegenkonto'); $blatt = Buchung::kontoblatt($this->tagesgeld); $this->assertEquals(1, count($blatt), 'Die Splitzeile erscheint als eigene Zeile im Kontoblatt'); $this->assertEquals(200.0, (float)$blatt[0]['betrag'], 'Mit umgekehrtem Vorzeichen'); $this->assertEquals('Rücklage', $blatt[0]['buchungstext'], 'Die Notiz der Zeile wird gezeigt'); Buchung::loeschen($id); $this->assertEquals(5000.0, Konto::saldo($this->tagesgeld), 'Löschen räumt auch die Gegensicht ab'); } private function salden(): void { Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-01-15', 'betrag' => -100.00, 'kategorie_id' => $this->kategorie, ]); // Eine Buchung in der Zukunft zählt nur zum erwarteten Saldo. $zukunft = date('Y-m-d', strtotime('+30 days')); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => $zukunft, 'betrag' => -250.00, 'kategorie_id' => $this->kategorie, ]); $konto = Konto::mitSalden(Konto::find($this->giro)); $this->assertEquals(900.0, $konto['saldo_aktuell'], 'Aktueller Saldo ohne künftige Buchungen'); $this->assertEquals(650.0, $konto['saldo_erwartet'], 'Erwarteter Saldo einschließlich'); // Nur Konten mit Σ-Kennzeichen zählen zur Summe. Konto::speichern($this->tagesgeld, [ 'name' => 'Testtagesgeld', 'kontoart' => 'sparkonto', 'anfangssaldo' => 5000, 'aktiv' => 1, 'in_summe' => 0, ]); $uebersicht = Konto::uebersicht(); $this->assertEquals(650.0, $uebersicht['summe'], 'Das Konto ohne Kennzeichen bleibt außen vor'); Konto::speichern($this->tagesgeld, [ 'name' => 'Testtagesgeld', 'kontoart' => 'sparkonto', 'anfangssaldo' => 5000, 'aktiv' => 1, 'in_summe' => 1, ]); $this->assertEquals(5650.0, Konto::uebersicht()['summe'], 'Mit Kennzeichen zählt es wieder mit'); } private function kontoblattFilter(): void { $miete = Kategorie::anlegen(['name' => 'Miete', 'art' => 'ausgabe']); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-06-01', 'betrag' => -800.00, 'empfaenger' => 'Vermieterin', 'kategorie_id' => $miete, 'abgeglichen' => true, ]); $alle = Buchung::kontoblatt($this->giro); $this->assertGreaterThan(2, count($alle), 'Das Kontoblatt zeigt alle Buchungen'); $gesucht = Buchung::kontoblatt($this->giro, ['suche' => 'Vermieter']); $this->assertEquals(1, count($gesucht), 'Volltextsuche über den Empfänger'); $nachKategorie = Buchung::kontoblatt($this->giro, ['kategorie_id' => $miete]); $this->assertEquals(1, count($nachKategorie), 'Filter auf die Kategorie'); $zeitraum = Buchung::kontoblatt($this->giro, ['von' => '2026-06-01', 'bis' => '2026-06-30']); $this->assertEquals(1, count($zeitraum), 'Filter auf den Zeitraum'); $offene = Buchung::kontoblatt($this->giro, ['nur_offene' => true]); foreach ($offene as $zeile) { $this->assertEquals(0, (int)$zeile['abgeglichen'], 'Filter zeigt nur nicht abgeglichene'); } // Ohne Filter läuft der Saldo mit, mit Filter wäre er irreführend. $this->assertNotEmpty($alle[0]['saldo'], 'Ungefiltert gibt es einen laufenden Saldo'); $this->assertNull($zeitraum[0]['saldo'], 'Gefiltert bleibt die Saldospalte leer'); } private function pruefungen(): void { $ohneKonto = false; try { Buchung::anlegen(['konto_id' => 0, 'datum' => '2026-01-01', 'betrag' => -1]); } catch (\Throwable) { $ohneKonto = true; } $this->assert($ohneKonto, 'Buchung ohne gültiges Konto wird abgewiesen'); $schlechtesDatum = false; try { Buchung::anlegen(['konto_id' => $this->giro, 'datum' => '18.03.2026', 'betrag' => -1]); } catch (\Throwable) { $schlechtesDatum = true; } $this->assert($schlechtesDatum, 'Datum im falschen Format wird abgewiesen'); $selbst = false; try { Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-01-01', 'betrag' => -1, 'gegenkonto_id' => $this->giro, ]); } catch (\Throwable) { $selbst = true; } $this->assert($selbst, 'Eine Umbuchung auf sich selbst wird abgewiesen'); $this->assert(Buchung::istDatum('2026-02-28'), 'Gültiges Datum'); $this->assert(!Buchung::istDatum('2026-02-30'), 'Der 30. Februar wird abgelehnt'); $this->assert(!Buchung::istDatum('2026-13-01'), 'Monat 13 wird abgelehnt'); $a = Buchung::hash(1, '2026-01-01', -10.0, '5', 'Text'); $b = Buchung::hash(1, '2026-01-01', -10.0, '5', 'Text'); $c = Buchung::hash(1, '2026-01-01', -10.01, '5', 'Text'); $this->assertEquals($a, $b, 'Gleiche Merkmale ergeben denselben Fingerabdruck'); $this->assert($a !== $c, 'Ein anderer Betrag ergibt einen anderen'); } }