mandantAnlegen(); $this->stammdaten(); $this->bestandFiltertAufEinPapier(); $this->bestandLiegtInBeidenDepots(); $this->kurseckwerteZeigenHochUndTief(); $this->datumsgrenzenUmfassenNurDiesesPapier(); $this->stueckOhneUeberfluessigeNullen(); $this->seiteZeigtStammdatenUndBuchungen(); $this->seiteFolgtDemZeitraum(); $this->uebersichtVerlinktDieDetailseite(); } private function stammdaten(): void { $this->depot = Konto::anlegen([ 'name' => 'Detaildepot', 'kontoart' => 'depot', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->zweitdepot = Konto::anlegen([ 'name' => 'Zweitdepot', 'kontoart' => 'depot', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->verrechnung = Konto::anlegen([ 'name' => 'Detailverrechnung', 'kontoart' => 'bank', 'anfangssaldo' => 50000, 'aktiv' => 1, ]); $this->papier = Wertpapier::findenOderAnlegen([ 'name' => 'Detailaktie AG', 'isin' => 'DE0007654321', 'wkn' => '765432', 'art' => 'aktie', ]); $this->anderes = Wertpapier::findenOderAnlegen([ 'name' => 'Nebenaktie SE', 'isin' => 'FR0001234567', 'art' => 'aktie', ]); // Zehn Stück zu 100 € plus 10 € Gebühren, dann fünf davon zu 130 € // verkauft, dazu eine Dividende – die Zahlen der Kennzahlenleiste. Depotbuchung::anlegen([ 'konto_id' => $this->depot, 'wertpapier_id' => $this->papier, 'datum' => '2026-01-10', 'vorgang' => 'kauf', 'stueck' => 10, 'kurs' => 100, 'betrag' => 1000, 'gebuehren' => 10, 'verrechnungskonto_id' => $this->verrechnung, ]); Depotbuchung::anlegen([ 'konto_id' => $this->depot, 'wertpapier_id' => $this->papier, 'datum' => '2026-03-20', 'vorgang' => 'verkauf', 'stueck' => 5, 'kurs' => 130, 'betrag' => 650, 'gebuehren' => 5, 'verrechnungskonto_id' => $this->verrechnung, ]); Depotbuchung::anlegen([ 'konto_id' => $this->depot, 'wertpapier_id' => $this->papier, 'datum' => '2026-05-02', 'vorgang' => 'dividende', 'betrag' => 40, 'steuern' => 10, 'verrechnungskonto_id' => $this->verrechnung, ]); // Dasselbe Papier ein zweites Mal, in einem anderen Depot. Depotbuchung::anlegen([ 'konto_id' => $this->zweitdepot, 'wertpapier_id' => $this->papier, 'datum' => '2026-06-15', 'vorgang' => 'kauf', 'stueck' => 4, 'kurs' => 125, 'betrag' => 500, 'gebuehren' => 0, 'verrechnungskonto_id' => $this->verrechnung, ]); // Ein fremdes Papier, das auf der Detailseite nichts zu suchen hat. Depotbuchung::anlegen([ 'konto_id' => $this->depot, 'wertpapier_id' => $this->anderes, 'datum' => '2025-11-11', 'vorgang' => 'kauf', 'stueck' => 3, 'kurs' => 50, 'betrag' => 150, 'verrechnungskonto_id' => $this->verrechnung, ]); Wertpapier::kursSetzen($this->papier, '2026-07-01', 120.0, 'manuell'); Wertpapier::kursSetzen($this->papier, '2026-08-01', 140.0, 'leeway'); Wertpapier::kursSetzen($this->anderes, '2026-08-01', 60.0, 'manuell'); } private function bestandFiltertAufEinPapier(): void { $bestand = Depot::bestand(null, '2026-12-31', $this->papier); foreach ($bestand['positionen'] as $position) { $this->assertEquals( $this->papier, $position['wertpapier_id'], 'Nur das gewählte Papier steht in den Positionen' ); } // 10 Stück für 1010 €, davon 5 verkauft: die Hälfte des Einstands geht // ab (505 €), der Erlös von 645 € ergibt 140 € realisiert. $this->assertEquals(140.0, $bestand['realisiert'], 'Der realisierte Gewinn gehört zu diesem Papier'); $this->assertEquals(30.0, $bestand['ertraege'], 'Die Dividende zählt netto'); $this->assertEquals(1005.0, $bestand['einstand'], '505 € aus dem ersten Depot, 500 € aus dem zweiten'); $this->assertEquals(1260.0, $bestand['wert'], 'Neun Stück zum letzten Kurs von 140 €'); } private function bestandLiegtInBeidenDepots(): void { $bestand = Depot::bestand(null, '2026-12-31', $this->papier); $this->assertEquals(2, count($bestand['positionen']), 'Je Depot eine Position'); $stueck = 0.0; foreach ($bestand['positionen'] as $position) { $stueck += (float)$position['stueck']; } $this->assertEquals(9.0, $stueck, 'Fünf im ersten, vier im zweiten Depot'); // Mit Depot eingegrenzt bleibt nur eine Position übrig. $eines = Depot::bestand($this->zweitdepot, '2026-12-31', $this->papier); $this->assertEquals(1, count($eines['positionen']), 'Depot und Papier lassen sich zugleich eingrenzen'); $this->assertEquals(4.0, $eines['positionen'][0]['stueck'], 'Vier Stück im Zweitdepot'); } private function kurseckwerteZeigenHochUndTief(): void { $eckwerte = Wertpapier::kurseckwerte($this->papier, '2026-01-01'); $this->assertNotEmpty($eckwerte, 'Es gibt Eckwerte'); $this->assertEquals(140.0, $eckwerte['hoch'], 'Der höchste Kurs'); $this->assertEquals(120.0, $eckwerte['tief'], 'Der niedrigste Kurs'); $this->assertEquals(2, $eckwerte['anzahl'], 'Zwei bekannte Kurse'); // Kurse anderer Papiere bleiben außen vor, ein leerer Zeitraum ergibt null. $this->assertEquals(60.0, Wertpapier::kurseckwerte($this->anderes, '2026-01-01')['hoch'], 'Je Papier gerechnet'); $this->assertNull(Wertpapier::kurseckwerte($this->papier, '2030-01-01'), 'Ohne Kurse im Zeitraum: null'); } private function datumsgrenzenUmfassenNurDiesesPapier(): void { $grenzen = Depotbuchung::datumsgrenzen($this->papier); $this->assertEquals('2026-01-10', $grenzen['von'], 'Erste Buchung des Papiers'); $this->assertEquals('2026-06-15', $grenzen['bis'], 'Letzte Buchung des Papiers'); // Ohne Papier zählen alle – das fremde von 2025 verschiebt den Anfang. $alle = Depotbuchung::datumsgrenzen(); $this->assertEquals('2025-11-11', $alle['von'], 'Über alle Papiere reicht es weiter zurück'); } private function stueckOhneUeberfluessigeNullen(): void { $this->assertEquals('12', View::stueck(12.0), 'Ganze Stücke ohne Nachkommastellen'); $this->assertEquals('0', View::stueck(0.0), 'Null bleibt eine Null'); $this->assertEquals('1,5', View::stueck(1.5), 'Bruchteile bleiben erhalten'); $this->assertEquals('1.234,25', View::stueck(1234.25), 'Tausenderpunkt wie bei Beträgen'); } private function seiteZeigtStammdatenUndBuchungen(): void { $seite = $this->detailseite('aktuelles_jahr'); $this->assert(str_contains($seite, 'Detailaktie AG'), 'Der Name steht auf der Seite'); $this->assert(str_contains($seite, 'DE0007654321'), 'Die ISIN steht auf der Seite'); $this->assert(str_contains($seite, '765432'), 'Die WKN steht auf der Seite'); $this->assert(str_contains($seite, 'Detaildepot'), 'Das Depot mit Bestand wird genannt'); $this->assert(str_contains($seite, 'Zweitdepot'), 'Auch das zweite Depot'); $this->assert(!str_contains($seite, 'Nebenaktie SE'), 'Fremde Papiere bleiben draußen'); // Kennzahlen und Kursangaben. $this->assert(str_contains($seite, '1.260,00'), 'Der bewertete Bestand steht in den Kennzahlen'); $this->assert(str_contains($seite, '140,00'), 'Der letzte Kurs steht in der Kurskachel'); $this->assert(str_contains($seite, 'leeway'), 'Die Quelle des Kurses wird genannt'); $this->assert(str_contains($seite, '10.01.2026'), 'Die erste Buchung wird genannt'); // Die Buchungen des laufenden Jahres, mit Vorgängen im Klartext. $this->assert(str_contains($seite, '20.03.2026'), 'Der Verkauf steht in der Liste'); $this->assert(str_contains($seite, 'Dividende'), 'Die Dividende steht in der Liste'); $this->assert( substr_count($seite, '/wertpapiere/papier/' . $this->papier) > 0, 'Der Zeitraumfilter zeigt auf dieselbe Seite' ); } private function seiteFolgtDemZeitraum(): void { // Das laufende Jahr ist 2026 – die Testdaten liegen darin. Ein Zeitraum // ohne Buchungen muss die Liste leeren, ohne die Kennzahlen anzutasten: // Die gelten für die ganze Historie. $leer = $this->detailseite('benutzerdefiniert', '2026-09-01', '2026-09-30'); $this->assert(str_contains($leer, 'Keine Buchungen in diesem Zeitraum'), 'Leerer Zeitraum, leere Liste'); $this->assert(!str_contains($leer, '20.03.2026'), 'Der Verkauf liegt außerhalb'); $this->assert(str_contains($leer, '1.260,00'), 'Die Kennzahlen bleiben die der ganzen Historie'); // Der Kauf vom Januar steht dann nur noch bei den Stammdaten als erste // Buchung, nicht mehr in der Liste darunter. $eng = $this->detailseite('benutzerdefiniert', '2026-03-01', '2026-03-31'); $this->assert(str_contains($eng, '20.03.2026'), 'Der Verkauf liegt im engen Zeitraum'); $this->assertEquals(1, substr_count($eng, '10.01.2026'), 'Der Kauf vom Januar nicht'); } private function uebersichtVerlinktDieDetailseite(): void { $bestand = Depot::bestand($this->depot, '2026-12-31'); $seite = $this->rendere('wertpapiere/index.php', [ 'titel' => 'Wertpapiere', 'depots' => Depot::depots(), 'depot' => $this->depot, 'bestand' => $bestand, 'entwicklung' => [], 'kursabruf' => false, ]); $this->assert( str_contains($seite, 'href="/wertpapiere/papier/' . $this->papier . '"'), 'Der Wertpapiername in der Übersicht führt zur Detailseite' ); } /** Rendert die Detailseite so, wie der Controller sie zusammenstellt. */ private function detailseite(string $vorgabe, string $von = '', string $bis = ''): string { $grenzen = Depotbuchung::datumsgrenzen($this->papier); $zeitraum = Zeitraum::berechne($vorgabe, $von, $bis, '2026-08-13', $grenzen); return $this->rendere('wertpapiere/papier.php', [ 'titel' => 'Detailaktie AG', 'wertpapier' => Wertpapier::find($this->papier), 'bestand' => Depot::bestand(null, '2026-12-31', $this->papier), 'kurs' => Wertpapier::letzterKurs($this->papier), 'eckwerte' => Wertpapier::kurseckwerte($this->papier, '2025-08-13'), 'entwicklung' => [], 'grenzen' => $grenzen, 'zeitraum' => $zeitraum, 'buchungen' => Depotbuchung::liste([ 'wertpapier_id' => $this->papier, 'von' => $zeitraum['von'], 'bis' => $zeitraum['bis'], ]), ]); } /** @param array $daten */ private function rendere(string $view, array $daten): string { $daten += ['aktuellerPfad' => '/wertpapiere', 'flash' => [], 'benutzer' => null, 'csrfToken' => 'x']; extract($daten, EXTR_SKIP); ob_start(); include dirname(__DIR__) . '/src/Views/' . $view; return (string)ob_get_clean(); } }