mandantAnlegen(); $this->stammdaten(); $this->buchungen(); $this->kachelauswahlMerktSich(); $this->monatsreiheTrenntEinnahmenUndAusgaben(); $this->monatsreiheLaesstUmbuchungenAus(); $this->verteilungFasstAufDerWurzelZusammen(); $this->verteilungKuerztAufDieGrenze(); $this->offeneVorgaengeZaehlenWasWartet(); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Cockpitgiro', 'kontoart' => 'bank', 'anfangssaldo' => 1000, 'aktiv' => 1, ]); $this->sparkonto = Konto::anlegen([ 'name' => 'Cockpitsparbuch', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->gehalt = Kategorie::findenOderAnlegenNachPfad('100 - Einkommen:Gehalt', 'einnahme'); $this->wohnen = Kategorie::findenOderAnlegenNachPfad('300 - Wohnen', 'ausgabe'); $this->miete = Kategorie::findenOderAnlegenNachPfad('300 - Wohnen:Miete', 'ausgabe'); $this->strom = Kategorie::findenOderAnlegenNachPfad('300 - Wohnen:Strom', 'ausgabe'); $this->freizeit = Kategorie::findenOderAnlegenNachPfad('500 - Freizeit', 'ausgabe'); } private function buchungen(): void { // Zwei Monate des laufenden Jahres, beide im Zwölfmonatsfenster. Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-07-01', 'betrag' => 3000, 'empfaenger' => 'Arbeitgeber', 'kategorie_id' => $this->gehalt, 'abgeglichen' => 1, ]); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-07-03', 'betrag' => -900, 'empfaenger' => 'Vermieter', 'kategorie_id' => $this->miete, 'abgeglichen' => 1, ]); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-07-15', 'betrag' => -100, 'empfaenger' => 'Stadtwerke', 'kategorie_id' => $this->strom, 'abgeglichen' => 1, ]); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-08-01', 'betrag' => 3000, 'empfaenger' => 'Arbeitgeber', 'kategorie_id' => $this->gehalt, 'abgeglichen' => 1, ]); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-08-05', 'betrag' => -200, 'empfaenger' => 'Kino', 'kategorie_id' => $this->freizeit, 'abgeglichen' => 1, ]); } private function kachelauswahlMerktSich(): void { $this->assertEquals( array_keys(Cockpit::KACHELN), Cockpit::sichtbar(), 'Ohne gespeicherte Auswahl sind alle Kacheln sichtbar' ); Cockpit::setzeSichtbar(['ausgaben', 'vermoegen', 'gibtesnicht']); $this->assertEquals( ['vermoegen', 'ausgaben'], Cockpit::sichtbar(), 'Gespeichert wird in Katalogreihenfolge, Unbekanntes fällt weg' ); $this->assert(Cockpit::zeigt('vermoegen'), 'Die gewählte Kachel wird gezeigt'); $this->assert(!Cockpit::zeigt('kontostaende'), 'Die abgewählte nicht'); // Alles abwählen muss möglich bleiben – sonst käme man aus einer leeren // Auswahl nie heraus, weil sie als „nie gespeichert“ gälte. Cockpit::setzeSichtbar([]); $this->assertEquals([], Cockpit::sichtbar(), 'Auch die leere Auswahl bleibt leer'); Cockpit::setzeSichtbar(array_keys(Cockpit::KACHELN)); } private function monatsreiheTrenntEinnahmenUndAusgaben(): void { $reihe = Cockpit::monatsreihe(self::HEUTE); $this->assertEquals(12, count($reihe['monate']), 'Zwölf Monatsspalten'); $this->assertEquals('2026-08', $reihe['monate'][11]['schluessel'], 'Die letzte Spalte ist der laufende Monat'); $this->assertEquals('2025-09', $reihe['monate'][0]['schluessel'], 'Die erste liegt elf Monate davor'); $this->assertEquals(3000.0, $reihe['einnahmen'][10], 'Juli: das Gehalt'); $this->assertEquals(1000.0, $reihe['ausgaben'][10], 'Juli: Miete und Strom, vorzeichenlos'); $this->assertEquals(2000.0, $reihe['salden'][10], 'Juli: Saldo ist Einnahmen minus Ausgaben'); $this->assertEquals(2800.0, $reihe['salden'][11], 'August: 3000 minus 200'); $this->assertEquals(6000.0, $reihe['summe_einnahmen'], 'Summe der Einnahmen über alle Monate'); $this->assertEquals(1200.0, $reihe['summe_ausgaben'], 'Summe der Ausgaben über alle Monate'); $this->assert($reihe['hatWerte'], 'Es gibt etwas zu zeichnen'); } /** Wie überall in den Auswertungen: Umbuchungen sind weder Einnahme noch Ausgabe. */ private function monatsreiheLaesstUmbuchungenAus(): void { $vorher = Cockpit::monatsreihe(self::HEUTE); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-08-06', 'betrag' => -500, 'empfaenger' => 'Aufs Sparbuch', 'gegenkonto_id' => $this->sparkonto, ]); $nachher = Cockpit::monatsreihe(self::HEUTE); $this->assertEquals( $vorher['summe_ausgaben'], $nachher['summe_ausgaben'], 'Die Umbuchung taucht in den Ausgaben nicht auf' ); $this->assertEquals( $vorher['salden'][11], $nachher['salden'][11], 'Und ändert den Saldo des Monats nicht' ); } /** * Die Kachel zeigt oberste Kategorien: „300 - Wohnen“ statt Miete und Strom * einzeln – sonst hätte der Ring so viele Segmente wie es Kategorien gibt. */ private function verteilungFasstAufDerWurzelZusammen(): void { $verteilung = Cockpit::ausgaben(self::HEUTE); $namen = array_column($verteilung['zeilen'], 'betrag', 'name'); $this->assertEquals(1000.0, $namen['300 - Wohnen'] ?? 0.0, 'Miete und Strom stehen unter ihrer Wurzel'); $this->assertEquals(200.0, $namen['500 - Freizeit'] ?? 0.0, 'Freizeit hat keine Kinder und steht für sich'); $this->assert(!isset($namen['300 - Wohnen:Miete']), 'Unterkategorien haben kein eigenes Segment'); $this->assertEquals(1200.0, $verteilung['summe'], 'Die Summe sind alle Ausgaben des Jahres'); $this->assertEquals('ausgabe', $verteilung['art'], 'Gefragt war nach Ausgaben'); $this->assert(!$verteilung['leer'], 'Die Kachel hat etwas zu zeigen'); $anteile = 0.0; foreach ($verteilung['zeilen'] as $zeile) { $this->assertGreaterThan(0.0, $zeile['betrag'], 'Jedes Segment trägt einen positiven Betrag'); $anteile += $zeile['anteil']; } $this->assertEquals(1.0, round($anteile, 6), 'Die Anteile ergeben zusammen den ganzen Ring'); // Einnahmen gehen genauso, nur andersherum. $einnahmen = Auswertung::verteilung( ['typ' => 'summenbericht', 'zeitraum' => 'aktuelles_jahr'], 'einnahme', 8, self::HEUTE ); $this->assertEquals(6000.0, $einnahmen['summe'], 'Die Einnahmenseite zählt das Gehalt'); } private function verteilungKuerztAufDieGrenze(): void { for ($nummer = 1; $nummer <= 12; $nummer++) { $kategorie = Kategorie::findenOderAnlegenNachPfad('9' . $nummer . ' - Klein ' . $nummer, 'ausgabe'); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-06-10', 'betrag' => -$nummer, 'empfaenger' => 'Kleinbetrag ' . $nummer, 'kategorie_id' => $kategorie, ]); } $verteilung = Auswertung::verteilung( ['typ' => 'summenbericht', 'zeitraum' => 'aktuelles_jahr'], 'ausgabe', 5, self::HEUTE ); $this->assertEquals(5, count($verteilung['zeilen']), 'Nicht mehr Segmente als die Grenze erlaubt'); $letzte = $verteilung['zeilen'][4]; $this->assert(str_starts_with($letzte['name'], 'Übrige ('), 'Das letzte Segment sammelt den Rest'); // Gekürzt wird die Darstellung, nicht die Summe. $gezeichnet = array_sum(array_column($verteilung['zeilen'], 'betrag')); $this->assertEquals( $verteilung['summe'], round($gezeichnet, 2), 'Die Segmente ergeben zusammen die Gesamtsumme' ); } private function offeneVorgaengeZaehlenWasWartet(): void { $schluessel = static fn(array $vorgaenge): array => array_column($vorgaenge, 'anzahl', 'schluessel'); $vorher = $schluessel(Cockpit::offeneVorgaenge(self::HEUTE)); Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-08-07', 'betrag' => -42, 'empfaenger' => 'Unbekannt', ]); $nachher = $schluessel(Cockpit::offeneVorgaenge(self::HEUTE)); $this->assertEquals( ($vorher['ohne_kategorie'] ?? 0) + 1, $nachher['ohne_kategorie'] ?? 0, 'Eine Buchung ohne Kategorie erscheint als offener Vorgang' ); $this->assertGreaterThan( 0, $nachher['nicht_abgeglichen'] ?? 0, 'Nicht abgeglichene Buchungen werden mitgezählt' ); // Umbuchungen brauchen keine Kategorie – sie sind keine offene Arbeit. $mitUmbuchung = $schluessel(Cockpit::offeneVorgaenge(self::HEUTE)); $this->assertEquals( $nachher['ohne_kategorie'] ?? 0, $mitUmbuchung['ohne_kategorie'] ?? 0, 'Die Umbuchung von vorhin zählt nicht als unkategorisiert' ); foreach (Cockpit::offeneVorgaenge(self::HEUTE) as $vorgang) { $this->assertNotEmpty($vorgang['pfad'], 'Jeder Vorgang führt irgendwohin'); $this->assertNotEmpty($vorgang['text'], 'Und trägt einen Text'); } } }