mandantAnlegen(); $this->stammdaten(); $this->vorlageBuchtMitAbweichendemBetrag(); $this->vorlageAusBuchung(); $this->qifVorlagenImport(); $this->regelmaessigeBuchungRuecktVor(); $this->faelligkeitslaufBuchtNurAutomatische(); $this->erinnerungenWerdenFaellig(); $this->ibanPruefungFaengtZahlendreher(); $this->vertragKenntSeineFrist(); $this->sparzielRechnetAusDemKonto(); $this->tilgungsplanTilgtBisNull(); $this->zahlungsauftragBuchtErstNachDerBank(); $this->sepaXmlIstVollstaendig(); $this->kalenderSammeltAlleQuellen(); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Nebengiro', 'kontoart' => 'bank', 'anfangssaldo' => 5000, 'aktiv' => 1, 'iban' => 'DE02120300000000202051', ]); $this->sparkonto = Konto::anlegen([ 'name' => 'Nebensparbuch', 'kontoart' => 'sparkonto', 'anfangssaldo' => 1000, 'aktiv' => 1, ]); $this->miete = Kategorie::findenOderAnlegenNachPfad('300 - Wohnen:Miete', 'ausgabe'); $this->strom = Kategorie::findenOderAnlegenNachPfad('300 - Wohnen:Strom', 'ausgabe'); } private function vorlageBuchtMitAbweichendemBetrag(): void { $id = Buchungsvorlage::anlegen([ 'name' => 'Stadtwerke Abschlag', 'konto_id' => $this->giro, 'betrag' => -100.00, 'empfaenger' => 'Stadtwerke', 'splits' => [ ['kategorie_id' => $this->strom, 'betrag' => -70.00], ['kategorie_id' => $this->miete, 'betrag' => -30.00], ], ]); // Zum Vorlagenbetrag: Die Splitzeilen bleiben, wie sie sind. $buchungId = Buchungsvorlage::buchen($id, ['datum' => '2026-08-01']); $splits = Buchung::splits($buchungId); $this->assertEquals(2, count($splits), 'Zwei Splitzeilen übernommen'); $this->assertEquals(-70.0, (float)$splits[0]['betrag'], 'Erste Zeile unverändert'); // Mit abweichendem Betrag: Die Zeilen werden im Verhältnis mitgezogen, // sonst passte die Splitsumme nicht mehr zum Buchungsbetrag. $zweite = Buchungsvorlage::buchen($id, ['datum' => '2026-08-02', 'betrag' => -150.00]); $splits = Buchung::splits($zweite); $this->assertEquals(-105.0, (float)$splits[0]['betrag'], 'Erste Zeile anteilig erhöht'); $this->assertEquals(-45.0, (float)$splits[1]['betrag'], 'Zweite Zeile ebenso'); $this->assertEquals( -150.0, round((float)$splits[0]['betrag'] + (float)$splits[1]['betrag'], 2), 'Und zusammen ergeben sie den Buchungsbetrag' ); // Krumme Verhältnisse: Die letzte Zeile bekommt den Rest. $dritte = Buchungsvorlage::buchen($id, ['datum' => '2026-08-03', 'betrag' => -33.33]); $splits = Buchung::splits($dritte); $this->assertEquals( -33.33, round((float)$splits[0]['betrag'] + (float)$splits[1]['betrag'], 2), 'Auch bei krummen Beträgen stimmt die Summe auf den Cent' ); $this->assertEquals(3, (int)Buchungsvorlage::find($id)['verwendet'], 'Der Zähler wurde geführt'); } private function vorlageAusBuchung(): void { $buchungId = Buchung::anlegen([ 'konto_id' => $this->giro, 'datum' => '2026-07-15', 'betrag' => -49.90, 'empfaenger' => 'Fitnessstudio', 'kategorie_id' => $this->miete, ]); $vorlageId = Buchungsvorlage::ausBuchung($buchungId, 'Fitnessstudio Beitrag'); $vorlage = Buchungsvorlage::find($vorlageId); $this->assertEquals(-49.90, (float)$vorlage['betrag'], 'Der Betrag kam mit'); $this->assertEquals('Fitnessstudio', $vorlage['empfaenger'], 'Der Empfänger auch'); $this->assertEquals($this->miete, (int)$vorlage['kategorie_id'], 'Und die Kategorie'); } private function qifVorlagenImport(): void { $qif = "!Option:MDY\r\n!Type:Memorized\r\n" . "KP\r\nU-11.99\r\nT-11.99\r\nP00455 MCDONALDS\r\nMFilialbesuch\r\n" . "L300 - Wohnen:Strom\r\n^\r\n" . "KP\r\nU-1,000.00\r\nT-1,000.00\r\nP.Deutsche.Bank.AG\r\nMVISA Debitkartenumsatz\r\n" . "L[Nebensparbuch]\r\n^\r\n"; $ergebnis = (new QifVorlagenImport(new QifParser($qif)))->ausfuehren(); $this->assertEquals(2, $ergebnis['neu'], 'Zwei Vorlagen übernommen'); $vorlagen = Buchungsvorlage::alle('MCDONALDS'); $this->assertEquals(1, count($vorlagen), 'Die erste ist wiederzufinden'); $this->assertEquals(-11.99, (float)$vorlagen[0]['betrag'], 'Mit ihrem Betrag'); $this->assertEquals($this->strom, (int)$vorlagen[0]['kategorie_id'], 'Und aufgelöster Kategorie'); // Ein Umbuchungsziel wird zum Gegenkonto, nicht zur Kategorie. $bank = Buchungsvorlage::alle('Deutsche.Bank'); $this->assertEquals($this->sparkonto, (int)$bank[0]['gegenkonto_id'], 'L[Konto] wird zum Gegenkonto'); $this->assertNull($bank[0]['kategorie_id'], 'Und nicht zur Kategorie'); // Ein zweiter Lauf legt nichts doppelt an. $zweiter = (new QifVorlagenImport(new QifParser($qif)))->ausfuehren(); $this->assertEquals(0, $zweiter['neu'], 'Der zweite Import legt nichts an'); $this->assertEquals(2, $zweiter['vorhanden'], 'Beide gelten als vorhanden'); } private function regelmaessigeBuchungRuecktVor(): void { $this->assertEquals( '2026-09-15', RegelmaessigeBuchung::naechsterTermin('2026-08-15', 'monatlich'), 'Monatlich springt einen Monat weiter' ); $this->assertEquals( '2026-10-15', RegelmaessigeBuchung::naechsterTermin('2026-08-15', 'monatlich', 2), 'Der Faktor überspringt Intervalle' ); $this->assertEquals( '2027-08-15', RegelmaessigeBuchung::naechsterTermin('2026-08-15', 'jaehrlich'), 'Jährlich springt ein Jahr' ); $id = RegelmaessigeBuchung::anlegen([ 'name' => 'Testmiete', 'konto_id' => $this->giro, 'betrag' => -850, 'empfaenger' => 'Vermieter', 'kategorie_id' => $this->miete, 'intervall' => 'monatlich', 'beginn' => '2026-06-01', 'aktiv' => 1, ]); $this->assertEquals( '2026-06-01', (string)RegelmaessigeBuchung::find($id)['naechste_faelligkeit'], 'Der erste Termin ist der Beginn' ); $buchungId = RegelmaessigeBuchung::buchen($id); $buchung = Buchung::find($buchungId); $this->assertEquals('2026-06-01', (string)$buchung['datum'], 'Gebucht wird zum Termin'); $this->assertEquals(-850.0, (float)$buchung['betrag'], 'Mit dem Betrag der Reihe'); // Nach dem Buchen steht der nächste Termin in der Zukunft – auch wenn // mehrere Termine zurücklagen, sonst käme derselbe immer wieder. $termin = (string)RegelmaessigeBuchung::find($id)['naechste_faelligkeit']; $this->assert($termin > date('Y-m-d'), 'Der nächste Termin liegt in der Zukunft'); // Überspringen bucht nicht, rückt aber vor. $vorher = Buchung::anzahl(); RegelmaessigeBuchung::ueberspringen($id); $this->assertEquals($vorher, Buchung::anzahl(), 'Überspringen bucht nichts'); $this->assert( (string)RegelmaessigeBuchung::find($id)['naechste_faelligkeit'] > $termin, 'Aber der Termin rückt weiter' ); // Eine Reihe mit Ende endet auch. $befristet = RegelmaessigeBuchung::anlegen([ 'name' => 'Befristete Rate', 'konto_id' => $this->giro, 'betrag' => -50, 'intervall' => 'monatlich', 'beginn' => '2026-07-01', 'ende' => '2026-07-31', 'aktiv' => 1, ]); RegelmaessigeBuchung::buchen($befristet); $this->assertEquals( 0, (int)RegelmaessigeBuchung::find($befristet)['aktiv'], 'Nach dem letzten Termin schaltet sich die Reihe ab' ); } private function faelligkeitslaufBuchtNurAutomatische(): void { $automatisch = RegelmaessigeBuchung::anlegen([ 'name' => 'Automatikbeitrag', 'konto_id' => $this->giro, 'betrag' => -20, 'intervall' => 'monatlich', 'beginn' => '2026-08-01', 'automatisch' => 1, 'aktiv' => 1, ]); $manuell = RegelmaessigeBuchung::anlegen([ 'name' => 'Stromabschlag', 'konto_id' => $this->giro, 'betrag' => -95, 'intervall' => 'monatlich', 'beginn' => '2026-08-01', 'automatisch' => 0, 'aktiv' => 1, ]); $vorschau = Faelligkeitslauf::vorschau(); $this->assertGreaterThan(0, $vorschau['faellig'], 'Es ist etwas fällig'); $vorher = Buchung::anzahl(); $ergebnis = Faelligkeitslauf::ausfuehren(); $this->assertGreaterThan(0, $ergebnis['gebucht'], 'Die automatische Reihe wurde gebucht'); $this->assertGreaterThan($vorher - 1, Buchung::anzahl(), 'Es gibt mehr Buchungen'); $namen = array_column($ergebnis['wartend'], 'name'); $this->assert(in_array('Stromabschlag', $namen, true), 'Die manuelle Reihe wartet auf Bestätigung'); // Ihr Termin ist unverändert – sie wurde ja nicht gebucht. $this->assertEquals( '2026-08-01', (string)RegelmaessigeBuchung::find($manuell)['naechste_faelligkeit'], 'Die wartende Reihe rückt nicht vor' ); $this->assert( (string)RegelmaessigeBuchung::find($automatisch)['naechste_faelligkeit'] > date('Y-m-d'), 'Die automatische schon' ); } private function erinnerungenWerdenFaellig(): void { $id = Erinnerung::anlegen('Versicherung kündigen', '2026-08-01'); $this->assertEquals(1, count(Erinnerung::faellige(self::HEUTE)), 'Eine Erinnerung ist fällig'); $this->assertEquals( 0, count(Erinnerung::faellige('2026-07-01')), 'Vor ihrem Datum noch nicht' ); Erinnerung::erledigen($id); $this->assertEquals(0, count(Erinnerung::faellige(self::HEUTE)), 'Erledigte sind nicht mehr fällig'); $this->assertEquals(0, Erinnerung::anzahlOffen(), 'Und zählen nicht mehr als offen'); Erinnerung::erledigen($id, false); $this->assertEquals(1, Erinnerung::anzahlOffen(), 'Zurückgeholt zählt sie wieder'); } private function ibanPruefungFaengtZahlendreher(): void { $this->assert(Adresse::ibanIstGueltig('DE02120300000000202051'), 'Eine gültige IBAN wird angenommen'); $this->assert(Adresse::ibanIstGueltig('DE02 1203 0000 0000 2020 51'), 'Auch mit Leerzeichen'); $this->assert(!Adresse::ibanIstGueltig('DE02120300000000202052'), 'Eine falsche Prüfziffer fällt auf'); $this->assert(!Adresse::ibanIstGueltig('DE0212030000000020205'), 'Zu kurz ist auch falsch'); $this->assert(!Adresse::ibanIstGueltig('XX02120300000000202051'), 'Unbekanntes Land ohne Prüfziffer'); $this->assert(!Adresse::ibanIstGueltig(''), 'Leer ist nicht gültig'); $this->assertEquals( 'DE02 1203 0000 0000 2020 51', Adresse::ibanFormatiert('DE02120300000000202051'), 'Formatiert in Vierergruppen' ); $id = Adresse::anlegen([ 'name' => 'Stadtwerke Musterstadt', 'iban' => 'DE02 1203 0000 0000 2020 51', 'ort' => 'Musterstadt', ]); $this->assertEquals( 'DE02120300000000202051', Adresse::find($id)['iban'], 'Gespeichert wird ohne Leerzeichen' ); $fehler = false; try { Adresse::anlegen(['name' => 'Falsche IBAN', 'iban' => 'DE02120300000000202052']); } catch (\InvalidArgumentException) { $fehler = true; } $this->assert($fehler, 'Eine ungültige IBAN wird abgewiesen'); } private function vertragKenntSeineFrist(): void { $id = Vertrag::anlegen([ 'name' => 'Hausratversicherung', 'partner' => 'Versicherung AG', 'betrag' => 12.50, 'intervall' => 'monatlich', 'beginn' => '2025-01-01', 'ende' => '2026-09-30', 'kuendigungsfrist_tage' => 90, 'aktiv' => 1, ]); $vertrag = Vertrag::find($id); $this->assertEquals(150.0, Vertrag::jahresbetrag($vertrag), 'Monatlich mal zwölf'); $this->assertEquals( '2026-07-02', Vertrag::kuendigenBis($vertrag), 'Kündigungstermin ist Ende minus Frist' ); // Ohne Ende oder ohne Frist gibt es keinen Termin. $this->assertNull( Vertrag::kuendigenBis(['ende' => null, 'kuendigungsfrist_tage' => 90]), 'Ohne Vertragsende kein Kündigungstermin' ); // Vierteljährlich gerechnet ergibt denselben Jahresbetrag bei viermal // dem Betrag. $this->assertEquals( 50.0, Vertrag::jahresbetrag(['betrag' => 12.50, 'intervall' => 'vierteljaehrlich']), 'Vierteljährlich mal vier' ); // Die Fristenliste greift nur innerhalb des Vorlaufs. $fristen = Vertrag::fristenLaufenAb(365, '2026-06-01'); $this->assertEquals(1, count($fristen), 'Innerhalb eines Jahres läuft die Frist ab'); $this->assertEquals( [], Vertrag::fristenLaufenAb(10, '2026-06-01'), 'In den nächsten zehn Tagen nicht' ); $this->assertEquals( [], Vertrag::fristenLaufenAb(365, '2026-08-01'), 'Und ein verpasster Termin taucht nicht mehr auf' ); } private function sparzielRechnetAusDemKonto(): void { $id = Sparziel::anlegen([ 'name' => 'Neues Auto', 'konto_id' => $this->sparkonto, 'zielbetrag' => 5000, 'startbetrag' => 1000, 'zieldatum' => '2027-08-01', ]); // Das Sparbuch trägt 1000 Anfangssaldo; der Startbetrag zieht sie ab. $ziel = Sparziel::mitStand(Sparziel::find($id), self::HEUTE); $this->assertEquals(0.0, $ziel['stand'], 'Zu Beginn ist nichts gespart'); Buchung::anlegen([ 'konto_id' => $this->sparkonto, 'datum' => '2026-07-01', 'betrag' => 1500, 'empfaenger' => 'Sparrate', ]); $ziel = Sparziel::mitStand(Sparziel::find($id), self::HEUTE); $this->assertEquals(1500.0, $ziel['stand'], 'Der Stand kommt aus dem Konto'); $this->assertEquals(3500.0, $ziel['fehlt'], 'Es fehlen 3500'); $this->assertEquals(0.3, round($ziel['fortschritt'], 2), 'Das sind 30 Prozent'); $this->assertNotEmpty($ziel['noetige_rate'], 'Und es gibt eine nötige Monatsrate'); // Zwölf Monate bis zum Ziel: 3500 / 12. $this->assertEquals(291.67, $ziel['noetige_rate'], 'Rate, um den Termin zu halten'); } private function tilgungsplanTilgtBisNull(): void { $id = Kredit::anlegen([ 'name' => 'Testdarlehen', 'darlehensbetrag' => 100000, 'zinssatz' => 3.0, 'rate' => 500, 'rhythmus' => 'monatlich', 'beginn' => '2026-01-01', 'zinsbindung_bis' => '2036-01-01', 'aktiv' => 1, ]); $plan = Kredit::tilgungsplan(Kredit::find($id)); $this->assert($plan['tilgt'], 'Der Kredit tilgt'); $this->assertGreaterThan(0, count($plan['zeilen']), 'Der Plan hat Zeilen'); $erste = $plan['zeilen'][0]; // 100000 × 3 % / 12 = 250 Zinsen, Rest tilgt. $this->assertEquals(250.0, $erste['zinsen'], 'Erste Rate: 250 Zinsen'); $this->assertEquals(250.0, $erste['tilgung'], 'Und 250 Tilgung'); $this->assertEquals(99750.0, $erste['restschuld'], 'Restschuld sinkt entsprechend'); $letzte = $plan['zeilen'][count($plan['zeilen']) - 1]; $this->assertEquals(0.0, $letzte['restschuld'], 'Am Ende ist die Restschuld null'); $this->assert($letzte['zahlung'] <= 500.0, 'Die letzte Rate ist höchstens eine volle'); // Der Zinsanteil sinkt, der Tilgungsanteil wächst – das Wesen der Annuität. $this->assert( $plan['zeilen'][12]['zinsen'] < $erste['zinsen'], 'Nach einem Jahr sind die Zinsen niedriger' ); $this->assert( $plan['zeilen'][12]['tilgung'] > $erste['tilgung'], 'Und der Tilgungsanteil höher' ); $this->assertNotEmpty($plan['restschuld_zinsbindung'], 'Die Restschuld zum Bindungsende steht dabei'); // Restschuld zu einem Stichtag. $this->assertEquals( 99750.0, Kredit::restschuld(Kredit::find($id), '2026-01-31'), 'Nach der ersten Rate' ); $this->assertEquals( 100000.0, Kredit::restschuld(Kredit::find($id), '2025-12-31'), 'Vor der ersten Rate steht das volle Darlehen' ); // Eine Rate unter den Zinsen tilgt nie – das muss auffallen, statt in // eine Endlosschleife zu laufen. $schlecht = Kredit::anlegen([ 'name' => 'Zu kleine Rate', 'darlehensbetrag' => 100000, 'zinssatz' => 5.0, 'rate' => 100, 'beginn' => '2026-01-01', ]); $planB = Kredit::tilgungsplan(Kredit::find($schlecht)); $this->assert(!$planB['tilgt'], 'Eine zu kleine Rate tilgt nicht'); $this->assertEquals([], $planB['zeilen'], 'Und erzeugt keinen Plan'); // Sondertilgung verkürzt die Laufzeit. $mitSonder = Kredit::anlegen([ 'name' => 'Mit Sondertilgung', 'darlehensbetrag' => 100000, 'zinssatz' => 3.0, 'rate' => 500, 'beginn' => '2026-01-01', 'sondertilgung' => 5000, ]); $planC = Kredit::tilgungsplan(Kredit::find($mitSonder)); $this->assert( count($planC['zeilen']) < count($plan['zeilen']), 'Mit Sondertilgung ist der Kredit früher abbezahlt' ); $this->assert($planC['zinsen'] < $plan['zinsen'], 'Und kostet weniger Zinsen'); } private function zahlungsauftragBuchtErstNachDerBank(): void { $id = Zahlungsauftrag::anlegen([ 'konto_id' => $this->giro, 'empfaenger' => 'Stadtwerke Musterstadt', 'iban' => 'DE02120300000000202051', 'betrag' => 84.20, 'verwendungszweck' => 'Abschlag Strom 08/2026', 'kategorie_id' => $this->strom, ]); $auftrag = Zahlungsauftrag::find($id); $this->assertEquals('entwurf', $auftrag['status'], 'Ein neuer Auftrag ist ein Entwurf'); $this->assertNull($auftrag['buchung_id'], 'Und hat noch keine Buchung'); $vorher = Buchung::anzahl(); Zahlungsauftrag::alsGesendetVermerken($id, 'Von der Bank angenommen.'); $auftrag = Zahlungsauftrag::find($id); $this->assertEquals('gesendet', $auftrag['status'], 'Danach gilt er als gesendet'); $this->assertEquals($vorher + 1, Buchung::anzahl(), 'Erst jetzt entsteht die Buchung'); $buchung = Buchung::find((int)$auftrag['buchung_id']); $this->assertEquals(-84.20, (float)$buchung['betrag'], 'Eine Überweisung geht ab'); $this->assertEquals($this->strom, (int)$buchung['kategorie_id'], 'Die Kategorie kam mit'); // Ein gesendeter Auftrag ist unveränderlich. $fehler = false; try { Zahlungsauftrag::speichern($id, [ 'konto_id' => $this->giro, 'empfaenger' => 'Anders', 'iban' => 'DE02120300000000202051', 'betrag' => 1, ]); } catch (\RuntimeException) { $fehler = true; } $this->assert($fehler, 'Ein gesendeter Auftrag lässt sich nicht ändern'); $fehler = false; try { Zahlungsauftrag::loeschen($id); } catch (\RuntimeException) { $fehler = true; } $this->assert($fehler, 'Und nicht löschen – er bleibt als Beleg'); // Ungültige Eingaben werden schon beim Erfassen abgewiesen. foreach ([ ['iban' => 'DE00000000000000000000', 'grund' => 'ungültige IBAN'], ['betrag' => 0, 'grund' => 'Betrag null'], ['ausfuehrung_am' => '2020-01-01', 'grund' => 'Termin in der Vergangenheit'], ] as $fall) { $daten = [ 'konto_id' => $this->giro, 'empfaenger' => 'Test', 'iban' => 'DE02120300000000202051', 'betrag' => 10.0, ]; unset($fall['grund']); $daten = array_merge($daten, $fall); $abgewiesen = false; try { Zahlungsauftrag::anlegen($daten); } catch (\InvalidArgumentException) { $abgewiesen = true; } $this->assert($abgewiesen, 'Abgewiesen: ' . json_encode($fall, JSON_UNESCAPED_UNICODE)); } } private function sepaXmlIstVollstaendig(): void { $xml = SepaXml::ueberweisung([ 'absender_name' => 'Max Mustermann', 'absender_iban' => 'DE02120300000000202051', 'empfaenger_name' => 'Stadtwerke Größenwahn', 'empfaenger_iban' => 'DE02500105170137075030', 'betrag' => 84.20, 'verwendungszweck' => 'Abschlag Strom & Wasser 08/2026', ]); $this->assert(str_contains($xml, 'pain.001.001.03'), 'Das erwartete Schema'); $this->assert(str_contains($xml, '84.20'), 'Betrag mit Währung'); $this->assert(str_contains($xml, 'DE02500105170137075030'), 'Empfänger-IBAN steht drin'); $this->assert(str_contains($xml, 'NOTPROVIDED'), 'Ohne BIC steht die Ersatzkennung'); // SEPA lässt keine Umlaute zu – sie werden umgeschrieben, nicht entfernt. $this->assert(str_contains($xml, 'Stadtwerke Groessenwahn'), 'Umlaute werden umgeschrieben'); $this->assert(!str_contains($xml, 'ö'), 'Und stehen nirgends mehr im Dokument'); // Das kaufmännische Und gehört nicht zum SEPA-Zeichenvorrat und fällt // weg – die Bank wiese den Auftrag sonst zurück, und zwar erst nach dem // Absenden. $this->assert(!str_contains($xml, '&'), 'Das kaufmännische Und fällt weg'); $this->assert( str_contains($xml, 'Abschlag Strom Wasser 08/2026'), 'Der Rest des Verwendungszwecks bleibt erhalten' ); $this->assertNotEmpty(simplexml_load_string($xml), 'Das Dokument ist wohlgeformtes XML'); $this->assertEquals('Test AG', SepaXml::text('Test AG', 70), 'Erlaubte Zeichen bleiben'); $this->assertEquals('Muellers Laden', SepaXml::text('Müllers Laden', 70), 'ü wird zu ue'); $this->assertEquals('Kurz', SepaXml::text('Kurz und knapp', 4), 'Und gekürzt wird auch'); $fehler = false; try { SepaXml::ueberweisung([ 'absender_name' => 'A', 'absender_iban' => 'DE02120300000000202051', 'empfaenger_name' => 'B', 'empfaenger_iban' => 'DE02500105170137075030', 'betrag' => 0, ]); } catch (\InvalidArgumentException) { $fehler = true; } $this->assert($fehler, 'Ein Betrag von null wird abgewiesen'); } private function kalenderSammeltAlleQuellen(): void { $termine = Finanzkalender::termine(90, self::HEUTE); $arten = array_unique(array_column($termine, 'art')); $this->assertGreaterThan(1, count($arten), 'Der Kalender führt mehrere Arten zusammen'); // Chronologisch sortiert. $daten = array_column($termine, 'datum'); $sortiert = $daten; sort($sortiert); $this->assertEquals($sortiert, $daten, 'Die Termine stehen chronologisch'); foreach ($termine as $termin) { $this->assertNotEmpty($termin['titel'], 'Jeder Termin hat einen Titel'); $this->assertNotEmpty($termin['pfad'], 'Und führt irgendwohin'); } $monate = Finanzkalender::nachMonat(90, self::HEUTE); $this->assertGreaterThan(0, count($monate), 'Nach Monaten gebündelt'); $gesamt = 0; foreach ($monate as $monat) { $this->assertNotEmpty($monat['titel'], 'Jeder Monat hat eine Überschrift'); $gesamt += count($monat['termine']); } $this->assertEquals(count($termine), $gesamt, 'Die Bündelung verliert keinen Termin'); } }