mandantAnlegen(); $this->stammdaten(); $this->zwoelfMonateBisHeute(); $this->staendeWachsenMonatFuerMonat(); $this->umbuchungVerschiebtZwischenKonten(); $this->summenkennzeichenFiltertNicht(); $this->schalterFiltertStillgelegteKonten(); $this->mehrKontenAlsFarbenWerdenZusammengefasst(); $this->ebenenBlaetternDurchDieUebrigenKonten(); $this->skalaRundetAufLesbareSchritte(); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Vermögensgiro', 'kontoart' => 'bank', 'anfangssaldo' => 1000, 'aktiv' => 1, ]); $this->sparkonto = Konto::anlegen([ 'name' => 'Vermögenssparbuch', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->kreditkarte = Konto::anlegen([ 'name' => 'Vermögenskarte', 'kontoart' => 'kreditkarte', 'anfangssaldo' => -200, 'aktiv' => 1, ]); // Durchlaufende Auslagen sind kein Vermögen und tragen kein Σ-Kennzeichen. $this->auslagen = Konto::anlegen([ 'name' => 'Vermögensauslagen', 'kontoart' => 'sonstiges', 'anfangssaldo' => 5000, 'aktiv' => 1, 'in_summe' => 0, ]); $this->kategorie = Kategorie::findenOderAnlegenNachPfad('600 - Vermögenstest', 'ausgabe'); } private function reihe(bool $nurAktive = true, int $ebene = 0): array { return Vermoegensentwicklung::reihe($nurAktive, 12, self::HEUTE, $ebene); } /** @return array|null */ private function konto(array $reihe, int $id): ?array { foreach ($reihe['konten'] as $konto) { if ($konto['id'] === $id) { return $konto; } } return null; } private function zwoelfMonateBisHeute(): void { $monate = Vermoegensentwicklung::monate(12, self::HEUTE); $this->assertEquals(12, count($monate), 'Zwölf Monate'); $this->assertEquals(2025, $monate[0]['jahr'], 'Der Rückblick beginnt im Vorjahr'); $this->assertEquals(9, $monate[0]['monat'], 'Zwölf Monate vor August 2026 ist September 2025'); $this->assertEquals(2026, $monate[11]['jahr'], 'Der letzte Balken ist der laufende Monat'); $this->assertEquals(8, $monate[11]['monat'], 'August 2026'); $this->assertEquals('2026-08-31', $monate[11]['bis'], 'Stichtag ist das Monatsende'); $this->assertEquals('2026-02-28', $monate[5]['bis'], 'Auch im Februar das echte Monatsende'); $this->assertEquals('Sep', $monate[0]['kuerzel'], 'Monatskürzel für die Achse'); } /** * Der Stand eines Monats ist der Stand des Vormonats plus dessen Bewegungen – * und Buchungen vor dem Zeitraum stecken im Anfangsstand des ersten Balkens. */ private function staendeWachsenMonatFuerMonat(): void { // Lange vor dem Zeitraum: zählt in den ersten Balken hinein. Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2024-05-10', 'betrag' => 500, 'empfaenger' => 'Altbestand', 'kategorie_id' => $this->kategorie, ]); // Im Zeitraum: hebt den Stand ab Dezember 2025. Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2025-12-05', 'betrag' => 300, 'empfaenger' => 'Zufluss', 'kategorie_id' => $this->kategorie, ]); // Nach dem letzten Balken: bleibt außen vor. Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-11-01', 'betrag' => 9999, 'empfaenger' => 'Zukunft', 'kategorie_id' => $this->kategorie, ]); $reihe = $this->reihe(); $giro = $this->konto($reihe, $this->giro); $this->assertNotEmpty($giro, 'Das Girokonto hat einen Balken'); $this->assertEquals(1500.0, $giro['werte'][0], 'September 2025: Anfangssaldo plus Altbestand'); $this->assertEquals(1500.0, $giro['werte'][2], 'November 2025 unverändert'); $this->assertEquals(1800.0, $giro['werte'][3], 'Dezember 2025 mit dem Zufluss'); $this->assertEquals(1800.0, $giro['werte'][11], 'Der Stand trägt bis in den letzten Monat'); // Die Kreditkarte steht im Minus – sie zieht die Summe herunter. $karte = $this->konto($reihe, $this->kreditkarte); $this->assertEquals(-200.0, $karte['werte'][11], 'Negativer Stand bleibt negativ'); $this->assert($reihe['hatWerte'], 'Es gibt etwas zu zeichnen'); // Giro 1800, Kreditkarte -200, Auslagen 5000, Sparbuch 0. $this->assertEquals(6600.0, $reihe['summen'][11], 'Die Summe ist der Stapel aller Konten'); } private function umbuchungVerschiebtZwischenKonten(): void { Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-03-15', 'betrag' => -400, 'empfaenger' => 'Umbuchung aufs Sparbuch', 'gegenkonto_id' => $this->sparkonto, ]); $reihe = $this->reihe(); $giro = $this->konto($reihe, $this->giro); $spar = $this->konto($reihe, $this->sparkonto); $this->assertEquals(1800.0, $giro['werte'][5], 'Februar 2026 noch ohne Umbuchung'); $this->assertEquals(1400.0, $giro['werte'][6], 'März 2026: 400 weniger auf dem Giro'); $this->assertEquals(400.0, $spar['werte'][6], 'Dieselben 400 liegen auf dem Sparbuch'); $this->assertEquals( $reihe['summen'][5], $reihe['summen'][6], 'Eine Umbuchung ändert das Vermögen nicht' ); } /** * Das Σ-Kennzeichen entscheidet hier nichts: Stillgelegte Konten tragen es * meist nicht, und dann fiele beim Ausschalten des Schalters genau das weg, * was man sehen will. */ private function summenkennzeichenFiltertNicht(): void { $reihe = $this->reihe(); $this->assertNotEmpty( $this->konto($reihe, $this->auslagen), 'Ein aktives Konto ohne Σ-Kennzeichen zeichnet trotzdem einen Balken' ); // Ein Konto, das im ganzen Zeitraum auf null steht, bläht nur die Legende auf. $leer = Konto::anlegen([ 'name' => 'Vermögensleerkonto', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->assertNull($this->konto($this->reihe(), $leer), 'Ein durchgehend leeres Konto fehlt'); } /** * Der Schalter „nur aktive“ an der Kachel: eingeschaltet zählt nur, was noch * geführt wird; ausgeschaltet auch das Vermögen auf stillgelegten Konten. */ private function schalterFiltertStillgelegteKonten(): void { $stillgelegt = Konto::anlegen([ 'name' => 'Vermögensaltkonto', 'kontoart' => 'bank', 'anfangssaldo' => 2500, 'aktiv' => 0, ]); $this->assertNull( $this->konto($this->reihe(), $stillgelegt), 'Eingeschaltet bleibt das stillgelegte Konto draußen' ); $alle = $this->reihe(false); $this->assertNotEmpty($this->konto($alle, $stillgelegt), 'Ausgeschaltet ist es dabei'); $this->assertEquals( 2500.0, $this->konto($alle, $stillgelegt)['werte'][11], 'Mit seinem Stand' ); $this->assertEquals( round($this->reihe()['summen'][11] + 2500.0, 2), $alle['summen'][11], 'Und die Summe wächst um genau diesen Stand' ); } /** * Zwölf Farbtöne hat die Palette; darüber hinaus behalten die größten Konten * ihre Farbe, der Rest wird zu einem Stück zusammengefasst. */ private function mehrKontenAlsFarbenWerdenZusammengefasst(): void { for ($nummer = 1; $nummer <= 15; $nummer++) { Konto::anlegen([ 'name' => 'Vermögensklein ' . $nummer, 'kontoart' => 'bank', 'anfangssaldo' => $nummer, 'aktiv' => 1, ]); } $reihe = $this->reihe(); $konten = $reihe['konten']; $letzte = $konten[count($konten) - 1]; $this->assertEquals( Vermoegensentwicklung::SERIEN, count($konten), 'Nicht mehr Balkenstücke als Farbtöne' ); $this->assertEquals(0, $letzte['id'], 'Das letzte Stück steht für kein einzelnes Konto'); $this->assert(str_starts_with($letzte['name'], 'Übrige Konten ('), 'Es heißt „Übrige Konten“'); // Die größten behalten ihre eigene Farbe – das Sparbuch ist dabei. $this->assertNotEmpty($this->konto($reihe, $this->sparkonto), 'Ein großes Konto bleibt eigenständig'); // Zusammengefasst wird, nicht weggelassen: Die Summe bleibt dieselbe. $gestapelt = 0.0; foreach ($konten as $konto) { $gestapelt += $konto['werte'][11]; } $this->assertEquals( $reihe['summen'][11], round($gestapelt, 2), 'Die Balkenstücke ergeben zusammen die Summe' ); } /** * „Übrige Konten“ ist keine Sackgasse: Jede Ebene zeigt die nächstkleineren * Konten einzeln, bis der Rest in die Palette passt. */ private function ebenenBlaetternDurchDieUebrigenKonten(): void { $this->assertEquals(1, Vermoegensentwicklung::ebenen(12), 'Was in die Palette passt, hat eine Ebene'); $this->assertEquals(2, Vermoegensentwicklung::ebenen(13), 'Ein Konto zu viel macht die zweite Ebene auf'); $this->assertEquals(2, Vermoegensentwicklung::ebenen(23), 'Elf eigene und zwölf übrige: noch zwei Ebenen'); $this->assertEquals(3, Vermoegensentwicklung::ebenen(24), 'Eines mehr, und es braucht eine dritte'); // Aus dem vorigen Test stehen mehr Konten da, als die Palette Töne hat. $erste = $this->reihe(true, 0); $zweite = $this->reihe(true, 1); $this->assertGreaterThan(1, $erste['ebenen'], 'Der Testbestand füllt mehr als eine Ebene'); $this->assertEquals(0, $erste['ebene'], 'Ohne Angabe steht man auf der ersten Ebene'); $this->assertEquals(1, $zweite['ebene'], 'Die zweite Ebene ist erreichbar'); $ids = static fn(array $reihe): array => array_map( static fn(array $konto): int => $konto['id'], $reihe['konten'] ); $this->assert(in_array(0, $ids($erste), true), 'Die erste Ebene sammelt den Rest ein'); $this->assertEquals( [], array_intersect(array_diff($ids($erste), [0]), $ids($zweite)), 'Kein Konto steht auf zwei Ebenen' ); // Die letzte Ebene braucht kein Sammelstück mehr – sonst käme man nie an. $letzte = $this->reihe(true, $erste['ebenen'] - 1); $this->assert(!in_array(0, $ids($letzte), true), 'Auf der letzten Ebene stehen nur noch echte Konten'); // Zusammen ergeben alle Ebenen wieder den ganzen Bestand. $gesehen = []; for ($ebene = 0; $ebene < $erste['ebenen']; $ebene++) { $gesehen = array_merge($gesehen, array_diff($ids($this->reihe(true, $ebene)), [0])); } $this->assertEquals( count($gesehen), count(array_unique($gesehen)), 'Über alle Ebenen hinweg taucht kein Konto doppelt auf' ); $this->assertGreaterThan( Vermoegensentwicklung::SERIEN, count($gesehen), 'Und in Summe sind es mehr Konten, als eine Ebene fasst' ); // Eine Ebene jenseits der letzten gibt es nicht: Sie fällt darauf zurück. $zuTief = $this->reihe(true, 99); $this->assertEquals($erste['ebenen'] - 1, $zuTief['ebene'], 'Zu tief geraten landet man auf der letzten'); $this->assertEquals($ids($letzte), $ids($zuTief), 'Mit denselben Konten'); } private function skalaRundetAufLesbareSchritte(): void { $skala = Vermoegensentwicklung::skala([0.0, 1234.0, 4870.0]); $this->assertEquals(0.0, $skala['min'], 'Die Null gehört immer dazu'); $this->assertGreaterThan(4869.0, $skala['max'], 'Der größte Wert passt hinein'); $this->assertEquals(0.0, fmod($skala['max'], $skala['schritt']), 'Das Maximum liegt auf einem Schritt'); $mitMinus = Vermoegensentwicklung::skala([-820.0, 2500.0]); $this->assert($mitMinus['min'] <= -820.0, 'Auch der negative Ausschlag passt hinein'); $this->assert($mitMinus['max'] >= 2500.0, 'Und der positive'); $leer = Vermoegensentwicklung::skala([]); $this->assertGreaterThan(0.0, $leer['schritt'], 'Ohne Werte bleibt die Skala brauchbar'); } }