mandantAnlegen(); $this->importMitStichtag(); $this->zweiterLaufAendertNichts(); $this->umbuchungsziel(); $this->kontenDerDatei(); $this->nurEinKonto(); } private function beispiel(): string { $zukunft = date('m.d.y', strtotime('+40 days')); $zeilen = [ '!Option:MDY', '!Type:Cat', 'N400 - Leben:Essen', 'E', '^', '!Option:AutoSwitch', '!Account', 'NGiro', 'TBank', '^', 'NTagesgeld', 'TOth T', '^', '!Clear:AutoSwitch', // Girokonto '!Account', 'NGiro', 'TBank', '^', '!Type:Bank ', // Eröffnungsbuchung: verweist auf das eigene Konto 'D1.2.18', 'T1,000.00', 'PEröffnungssaldo', 'L[Giro]', 'CX', '^', // vor dem Stichtag: fließt in den Anfangssaldo 'D6.15.25', 'T-100.00', 'PBäcker', 'L400 - Leben:Essen', 'CX', '^', // Umbuchung vor dem Stichtag – die Gegenseite steht nirgends 'D7.1.25', 'T-400.00', 'PÜbertrag', 'L[Tagesgeld]', 'CX', '^', // im Zeitraum 'D3.18.26', 'U-49.90', 'T-49.90', 'PSupermarkt', 'MWocheneinkauf', 'L400 - Leben:Essen/Privat', 'CX', 'N77', '^', // Umbuchung im Zeitraum 'D4.1.26', 'T-250.00', 'PÜbertrag', 'L[Tagesgeld]', '^', // Splitbuchung im Zeitraum 'D5.2.26', 'T-300.00', 'PGemischt', 'L400 - Leben:Essen', 'S400 - Leben:Essen', 'EEinkauf', '$-100.00', 'S[Tagesgeld]', 'ERücklage', '$-200.00', '^', // nach dem Enddatum: vorausberechnet 'D' . $zukunft, 'T-999.00', 'PVorausschau', 'L400 - Leben:Essen', '^', ]; return implode("\r\n", $zeilen) . "\r\n"; } private function importMitStichtag(): void { $parser = new QifParser($this->beispiel()); (new QifStammdatenImport($parser))->ausfuehren(); $ergebnis = (new QifBuchungsImport($parser, [ 'stichtag' => '2026-01-01', 'bis' => date('Y-m-d'), ]))->ausfuehren(); $this->assertEquals(3, $ergebnis['buchungen']['neu'], 'Drei Buchungen im Zeitraum'); $this->assertEquals(3, $ergebnis['buchungen']['vor_stichtag'], 'Drei Buchungen davor verrechnet'); $this->assertEquals(1, $ergebnis['buchungen']['zukunft'], 'Eine vorausberechnete Buchung verworfen'); $this->assertEquals(0, $ergebnis['buchungen']['fehler'], 'Keine Fehler'); $giro = Konto::findeNachName('Giro'); $tagesgeld = Konto::findeNachName('Tagesgeld'); // Anfangssaldo Giro: 1000 (Eröffnung) - 100 (Bäcker) - 400 (Übertrag) $this->assertEquals('500.00', $giro['anfangssaldo'], 'Anfangssaldo aus der Vorgeschichte'); // Die Gegenseite der Umbuchung steht nirgends in der Datei und muss // trotzdem auf dem Tagesgeldkonto ankommen. $this->assertEquals('400.00', $tagesgeld['anfangssaldo'], 'Erzeugte Gegenbuchung im Anfangssaldo'); // Saldo Giro: 500 - 49,90 - 250 - 300 $this->assertEquals(-99.90, round(Konto::saldo((int)$giro['id']), 2), 'Girosaldo nach dem Import'); // Tagesgeld: 400 + 250 (Umbuchung) + 200 (Splitzeile) $this->assertEquals(850.0, round(Konto::saldo((int)$tagesgeld['id']), 2), 'Tagesgeldsaldo nach dem Import'); $blatt = Buchung::kontoblatt((int)$tagesgeld['id']); $this->assertEquals(2, count($blatt), 'Beide Gegensichten stehen im Kontoblatt'); foreach ($blatt as $zeile) { $this->assert($zeile['gegensicht'], 'Es sind Gegensichten, keine eigenen Buchungen'); } $supermarkt = null; foreach (Buchung::kontoblatt((int)$giro['id']) as $zeile) { if ($zeile['empfaenger'] === 'Supermarkt') { $supermarkt = $zeile; } } $this->assertNotEmpty($supermarkt, 'Die Buchung im Zeitraum ist da'); $this->assertEquals('Wocheneinkauf', $supermarkt['buchungstext'], 'Buchungstext übernommen'); $this->assertEquals('77', $supermarkt['nummer'], 'Nummer übernommen'); $this->assertEquals(1, (int)$supermarkt['abgeglichen'], 'Abgleichstatus aus dem C-Feld'); $this->assertNotEmpty($supermarkt['klasse_id'], 'Die Klasse hinter dem Schrägstrich wurde erkannt'); $this->assertEquals( '400 - Leben:Essen', Kategorie::find((int)$supermarkt['kategorie_id'])['pfad'], 'Kategorie zugeordnet' ); // Die Eröffnungsbuchung verweist auf ihr eigenes Konto und darf keine // Umbuchung geworden sein – sonst hübe sie sich selbst auf. $this->assertEquals( '500.00', Konto::findeNachName('Giro')['anfangssaldo'], 'Die Eröffnungsbuchung zählt einfach, nicht doppelt' ); } private function zweiterLaufAendertNichts(): void { $vorher = Buchung::anzahl(); $giro = (float)Konto::findeNachName('Giro')['anfangssaldo']; $parser = new QifParser($this->beispiel()); $ergebnis = (new QifBuchungsImport($parser, [ 'stichtag' => '2026-01-01', 'bis' => date('Y-m-d'), ]))->ausfuehren(); $this->assertEquals(0, $ergebnis['buchungen']['neu'], 'Zweiter Lauf legt nichts an'); $this->assertEquals(3, $ergebnis['buchungen']['vorhanden'], 'Alle drei als vorhanden erkannt'); $this->assertEquals($vorher, Buchung::anzahl(), 'Die Zahl der Buchungen ist unverändert'); $this->assertEquals($giro, (float)Konto::findeNachName('Giro')['anfangssaldo'], 'Anfangssaldo bleibt gleich'); } private function kontenDerDatei(): void { $konten = QifBuchungsImport::kontenDerDatei(new QifParser($this->beispiel())); $this->assertEquals(['Giro', 'Tagesgeld'], array_keys($konten), 'Beide Konten stehen zur Wahl'); $this->assertEquals(7, $konten['Giro'], 'Alle Einträge des Girokontos gezählt'); // Tagesgeld führt keine eigene Buchung: die drei Einträge zeigen von // Giro aus hierher (zwei Umbuchungen, eine Splitzeile). $this->assertEquals(3, $konten['Tagesgeld'], 'Ein reines Umbuchungsziel steht ebenfalls zur Wahl'); } /** * Teilimport: erst nur das Tagesgeldkonto, dann das Girokonto. Der zweite * Lauf darf die Umbuchungen nicht ein zweites Mal anlegen – auch nicht mit * anderer Kontoauswahl. */ private function nurEinKonto(): void { $this->mandantAnlegen(); $parser = new QifParser($this->beispiel()); (new QifStammdatenImport($parser))->ausfuehren(); $ergebnis = (new QifBuchungsImport($parser, [ 'stichtag' => '2026-01-01', 'bis' => date('Y-m-d'), 'konten' => ['Tagesgeld'], ]))->ausfuehren(); $this->assertEquals(2, $ergebnis['buchungen']['neu'], 'Nur die beiden Buchungen mit Bezug zum Tagesgeld'); $this->assertEquals(1, $ergebnis['buchungen']['nicht_gewaehlt'], 'Der Supermarkt gehört allein zum Giro'); $this->assertEquals(0, $ergebnis['buchungen']['fehler'], 'Keine Fehler beim Teilimport'); $this->assertEquals(1, $ergebnis['anfangssalden']['konten'], 'Nur ein Anfangssaldo geschrieben'); $giro = Konto::findeNachName('Giro'); $tagesgeld = Konto::findeNachName('Tagesgeld'); $this->assertEquals('400.00', $tagesgeld['anfangssaldo'], 'Anfangssaldo des gewählten Kontos'); $this->assertEquals('0.00', $giro['anfangssaldo'], 'Das nicht gewählte Konto bleibt unangetastet'); $this->assertEquals(850.0, round(Konto::saldo((int)$tagesgeld['id']), 2), 'Tagesgeldsaldo aus den Gegensichten'); // Zweiter Lauf, diesmal das Girokonto: Die Umbuchungen sind schon da – // ihr Fingerabdruck darf nicht von der Auswahl abhängen. $ergebnis = (new QifBuchungsImport($parser, [ 'stichtag' => '2026-01-01', 'bis' => date('Y-m-d'), 'konten' => ['Giro'], ]))->ausfuehren(); $this->assertEquals(1, $ergebnis['buchungen']['neu'], 'Nur der Supermarkt fehlte noch'); $this->assertEquals(2, $ergebnis['buchungen']['vorhanden'], 'Die Umbuchungen waren schon da'); $this->assertEquals(3, Buchung::anzahl(), 'Keine Buchung doppelt angelegt'); $this->assertEquals( '500.00', Konto::findeNachName('Giro')['anfangssaldo'], 'Jetzt bekommt auch das Girokonto seinen Anfangssaldo' ); $this->assertEquals(-99.90, round(Konto::saldo((int)$giro['id']), 2), 'Girosaldo wie beim Vollimport'); } private function umbuchungsziel(): void { $this->assertEquals('DKB Girokonto', QifBuchungsImport::umbuchungsziel('[DKB Girokonto]'), 'Einfache Umbuchung'); $this->assertEquals('DKB Girokonto', QifBuchungsImport::umbuchungsziel('|[DKB Girokonto]'), 'Wertpapierschreibweise'); $this->assertNull(QifBuchungsImport::umbuchungsziel('400 - Leben:Essen'), 'Kategorie ist kein Konto'); $this->assertNull(QifBuchungsImport::umbuchungsziel(null), 'Fehlende Angabe'); $this->assertNull(QifBuchungsImport::umbuchungsziel('[unvollständig'), 'Fehlende schließende Klammer'); } }