mandantAnlegen(); $this->stammdaten(); $this->vorschauLegtNichtsAn(); $this->importLegtBestandUndKurseAn(); $this->zweiterLaufLegtNichtsDoppeltAn(); $this->depotWirdUeberDieDepotnummerGefunden(); $this->verrechnungskontoIstFreiwillig(); $this->fremdeDateiWirdAbgewiesen(); $this->ausDateiLiestVomDatentraeger(); } private function stammdaten(): void { $this->depot = Konto::anlegen([ 'name' => 'CSV-Depot', 'kontoart' => 'depot', 'kontonummer' => '502022353', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->verrechnung = Konto::anlegen([ 'name' => 'CSV-Verrechnung', 'kontoart' => 'bank', 'anfangssaldo' => 5000, 'aktiv' => 1, ]); } private function import(array $optionen = []): DepotbestandImport { return new DepotbestandImport(self::CSV, $optionen + ['konto_id' => $this->depot]); } private function vorschauLegtNichtsAn(): void { $ergebnis = $this->import()->ausfuehren(true); $this->assertEquals(3, $ergebnis['zaehler']['neu'], 'Drei Positionen in der Vorschau'); $this->assertEquals(3, $ergebnis['zaehler']['wertpapiereNeu'], 'Drei Wertpapiere wären neu'); $this->assertEquals('2026-08-12', $ergebnis['datum'], 'Der Stichtag kommt aus der Datei'); $this->assertEquals('502022353', $ergebnis['depotnummer'], 'Die Depotnummer wird gelesen'); $this->assertEquals(0, Depotbuchung::anzahl(), 'Der Trockenlauf hat nichts gespeichert'); $this->assertEquals(0, Wertpapier::anzahl(), 'Und auch kein Wertpapier angelegt'); // 60 × 224,60 + 55,6863 × 58,36 + 3 × 648,33 = 13.476,00 + 3.249,85 + 1.944,99. $this->assertEquals(18670.84, $ergebnis['einstand'], 'Einstand aus Stück × Einstiegskurs'); // 60 × 437,30 + 55,6863 × 75,19 + 3 × 522,50 = 26.238,00 + 4.187,05 + 1.567,50. $this->assertEquals(31992.55, $ergebnis['wert'], 'Wert aus Stück × Bewertungskurs'); } private function importLegtBestandUndKurseAn(): void { $ergebnis = $this->import()->ausfuehren(); $this->assertEquals(3, $ergebnis['zaehler']['neu'], 'Drei Positionen übernommen'); $this->assertEquals(3, $ergebnis['zaehler']['kurse'], 'Drei Bewertungskurse hinterlegt'); $this->assertEquals(0, $ergebnis['zaehler']['fehler'], 'Keine Fehler'); $buchungen = Depotbuchung::liste(['konto_id' => $this->depot]); $this->assertEquals(3, count($buchungen), 'Drei Depotbuchungen'); foreach ($buchungen as $buchung) { $this->assertEquals('kauf', (string)$buchung['vorgang'], 'Alles wird als Kauf gebucht'); $this->assertEquals('2026-08-12', (string)$buchung['datum'], 'Und zwar zum Stichtag'); $this->assertNull($buchung['buchung_id'], 'Ohne Verrechnungskonto keine Geldbuchung'); } // Bruchstückzahlen kommen mit Komma und dürfen nicht auf ganze Stücke fallen. $etf = Wertpapier::finde('LU1834983477'); $this->assertNotEmpty($etf, 'Der ETF wurde angelegt'); $this->assertEquals('fonds', (string)$etf['art'], 'Assetklasse ETFs wird zur Art Fonds'); $bestand = Depot::bestand($this->depot, '2026-08-12'); $this->assertEquals(3, count($bestand['positionen']), 'Drei Positionen im Bestand'); $this->assert($bestand['bewertbar'], 'Der Bestand ist sofort bewertbar'); $this->assertEquals(18670.84, $bestand['einstand'], 'Der Einstand entspricht dem Export'); $this->assertEquals(31992.55, $bestand['wert'], 'Der Wert ebenso'); // Die Bank weist in Summe 13.321,65 € aus – die Differenz von sechs Cent // steckt in den auf zwei Stellen gerundeten Kursen des Exports. $this->assertEquals(13321.71, $bestand['gewinn'], 'Und damit auch der Gewinn'); $allianz = Wertpapier::finde('DE0008404005'); $kurs = Wertpapier::letzterKurs((int)$allianz['id'], '2026-08-12'); $this->assertEquals(437.30, (float)$kurs['kurs'], 'Der Bewertungskurs steht als Kurs des Stichtags'); $position = null; foreach ($bestand['positionen'] as $eintrag) { $position = $eintrag['isin'] === 'LU1834983477' ? $eintrag : $position; } $this->assertEquals(55.6863, (float)$position['stueck'], 'Bruchstücke bleiben erhalten'); } private function zweiterLaufLegtNichtsDoppeltAn(): void { $ergebnis = $this->import()->ausfuehren(); $this->assertEquals(0, $ergebnis['zaehler']['neu'], 'Der zweite Lauf legt nichts an'); $this->assertEquals(3, $ergebnis['zaehler']['vorhanden'], 'Alle drei sind schon da'); $this->assertEquals(3, count(Depotbuchung::liste(['konto_id' => $this->depot])), 'Es bleiben drei'); // Ein anderer Stichtag ist ein anderer Vorgang – dann darf gebucht werden. $spaeter = $this->import(['datum' => '2026-08-13'])->ausfuehren(true); $this->assertEquals(3, $spaeter['zaehler']['neu'], 'Zu einem anderen Datum wäre alles neu'); $this->assertEquals(0, $spaeter['zaehler']['wertpapiereNeu'], 'Die Wertpapiere gibt es aber schon'); } private function depotWirdUeberDieDepotnummerGefunden(): void { $passend = DepotbestandImport::passendesDepot('502022353'); $this->assertEquals($this->depot, (int)$passend['id'], 'Die Kontonummer führt zum Depot'); $this->assertNull(DepotbestandImport::passendesDepot('999'), 'Eine fremde Nummer findet nichts'); $this->assertNull(DepotbestandImport::passendesDepot(''), 'Ohne Nummer wird nicht geraten'); } private function verrechnungskontoIstFreiwillig(): void { $zweitesDepot = Konto::anlegen([ 'name' => 'CSV-Depot mit Geldseite', 'kontoart' => 'depot', 'aktiv' => 1, ]); (new DepotbestandImport(self::CSV, [ 'konto_id' => $zweitesDepot, 'verrechnungskonto_id' => $this->verrechnung, ]))->ausfuehren(); $buchungen = Depotbuchung::liste(['konto_id' => $zweitesDepot]); $this->assertEquals(3, count($buchungen), 'Auch hier drei Depotbuchungen'); $this->assertNotEmpty($buchungen[0]['buchung_id'], 'Diesmal mit Geldbuchung'); // Der volle Einstand belastet das Verrechnungskonto. $saldo = Konto::saldo($this->verrechnung, '2026-08-12'); $this->assertEquals(5000.0 - 18670.84, round($saldo, 2), 'Der Einstand wurde abgebucht'); } private function fremdeDateiWirdAbgewiesen(): void { $fehler = ''; try { new DepotbestandImport("Datum;Empfänger;Betrag\n01.01.2026;Miete;-800,00\n", [ 'konto_id' => $this->depot, ]); } catch (\Throwable $e) { $fehler = $e->getMessage(); } $this->assert(str_contains($fehler, 'Depotbestandsliste'), 'Eine Umsatzliste wird erkannt'); $fehler = ''; try { new DepotbestandImport('', ['konto_id' => $this->depot]); } catch (\Throwable $e) { $fehler = $e->getMessage(); } $this->assert($fehler !== '', 'Eine leere Datei ebenso'); // Ohne gültiges Depot wird gar nicht erst gelesen. $fehler = ''; try { (new DepotbestandImport(self::CSV, ['konto_id' => 0]))->ausfuehren(true); } catch (\Throwable $e) { $fehler = $e->getMessage(); } $this->assert(str_contains($fehler, 'Depot'), 'Ohne Depot bricht der Import ab'); } private function ausDateiLiestVomDatentraeger(): void { $pfad = $this->tempPfad('.csv'); file_put_contents($pfad, self::CSV); $ergebnis = DepotbestandImport::ausDatei($pfad, ['konto_id' => $this->depot])->ausfuehren(true); $this->assertEquals(3, count($ergebnis['positionen']), 'Auch aus der Datei kommen drei Positionen'); } }