mandantAnlegen(); $this->stammdaten(); $this->geloeschteBuchungGibtDenUmsatzFrei(); $this->verwaisterUmsatzWirdWiederGeoeffnet(); $this->fingerabdruckHaengtNichtAnDerIban(); $this->bankkontoOhneIbanFindetSichUeberDieKontonummer(); $this->nachgelieferteIbanWirdNachgetragen(); } private function stammdaten(): void { $this->giro = Konto::anlegen([ 'name' => 'Verwaisungsgiro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1, ]); $this->zugang = Bankzugang::anlegen([ 'name' => 'Verwaisungsbank', 'bankname' => 'Verwaisungsbank AG', 'blz' => '20041111', 'fints_url' => 'https://banking.example.org/fints', 'benutzerkennung' => 'testkunde', 'pin' => 'geheim1234', 'abrufzeitraum' => 'letzter_abruf', 'aktiv' => 1, ]); $this->bankkonto = (int)Bankzugang::findeOderLegeKontoAn( $this->zugang, 'DE53200411110847626900', '847626900', 'Girokonto' )['id']; Bankzugang::ordneKontoZu($this->bankkonto, $this->giro); } private function umsatzAnlegen(string $zweck, float $betrag = -12.34): int { $id = Bankumsatz::anlegen([ 'zugang_id' => $this->zugang, 'konto_id' => $this->giro, 'bank_konto_id' => $this->bankkonto, 'extern_id' => Bankumsatz::externId($this->bankkonto, '2026-08-05', $betrag, $zweck), 'buchungstag' => '2026-08-05', 'wertstellung' => '2026-08-05', 'betrag' => $betrag, 'empfaenger' => 'Testempfänger', 'verwendungszweck' => $zweck, 'buchungsart' => 'LASTSCHRIFT', ]); $this->assertNotEmpty($id, 'Umsatz angelegt: ' . $zweck); return (int)$id; } private function status(int $umsatzId): string { return (string)Database::fetchValue( 'SELECT status FROM bank_umsaetze WHERE id = ?', [$umsatzId], '' ); } /** * Der eigentliche Fehler: Wer eine gebuchte Zahlung im Kontoblatt löscht, * bekam den Umsatz nie wieder zu Gesicht. */ private function geloeschteBuchungGibtDenUmsatzFrei(): void { $umsatz = $this->umsatzAnlegen('Strom August'); $buchungId = Bankumsatz::uebernehmen($umsatz, []); $this->assertEquals('uebernommen', $this->status($umsatz), 'Nach dem Übernehmen ist der Umsatz übernommen'); Buchung::loeschen((int)$buchungId); $this->assertEquals('offen', $this->status($umsatz), 'Gelöschte Buchung gibt den Umsatz wieder frei'); $this->assertNull( Database::fetchValue('SELECT buchung_id FROM bank_umsaetze WHERE id = ?', [$umsatz]), 'Der Umsatz zeigt auf keine Buchung mehr' ); $offene = array_column(Bankumsatz::liste(['status' => 'offen']), 'id'); $this->assert(in_array((string)$umsatz, array_map('strval', $offene), true), 'Der Umsatz steht wieder in der Vorschlagsliste'); } /** * Nicht jede Buchung verschwindet über `Buchung::loeschen()`: Wird ein Konto * gelöscht, räumt der Fremdschlüssel seine Buchungen per CASCADE ab – und * Kaskaden lösen in MariaDB keine Trigger aus. Dieser Zustand muss sich von * selbst wieder auflösen. */ private function verwaisterUmsatzWirdWiederGeoeffnet(): void { $umsatz = $this->umsatzAnlegen('Kaskadenlastschrift', -8.39); Bankumsatz::uebernehmen($umsatz, []); // Genau der Zustand, den eine Kaskade hinterlässt. Database::execute( "UPDATE bank_umsaetze SET buchung_id = NULL WHERE id = ?", [$umsatz] ); $this->assertEquals('uebernommen', $this->status($umsatz), 'Die Kaskade lässt den Status stehen'); $anzahl = Bankumsatz::verwaisteOeffnen(); $this->assertGreaterThan(0, $anzahl, 'Mindestens ein verwaister Umsatz wurde geöffnet'); $this->assertEquals('offen', $this->status($umsatz), 'Der verwaiste Umsatz ist wieder offen'); } /** * Die Comdirect nennt die IBAN beim Kontenabruf, beim Umsatzabruf aber * nicht. Hing der Fingerabdruck daran, kam derselbe Umsatz ein zweites Mal * herein. */ private function fingerabdruckHaengtNichtAnDerIban(): void { $mitIban = Bankumsatz::externId($this->bankkonto, '2026-08-07', -22.00, 'Kiosk'); $erneut = Bankumsatz::externId($this->bankkonto, '2026-08-07', -22.00, 'Kiosk'); $this->assertEquals($mitIban, $erneut, 'Derselbe Umsatz ergibt denselben Fingerabdruck'); $anderesKonto = Bankumsatz::externId($this->bankkonto + 1, '2026-08-07', -22.00, 'Kiosk'); $this->assert($mitIban !== $anderesKonto, 'Ein anderes Bankkonto ergibt einen anderen Fingerabdruck'); } /** * Liefert die Bank beim zweiten Dialog keine IBAN mehr, darf daraus kein * zweites Bankkonto entstehen – sonst hätte das Konto zwei Zuordnungen und * zwei Fingerabdrücke, und jeder Umsatz stünde doppelt da. */ private function bankkontoOhneIbanFindetSichUeberDieKontonummer(): void { $ohneIban = Bankzugang::findeOderLegeKontoAn($this->zugang, '', '847626900', '847626900'); $this->assertEquals( $this->bankkonto, (int)$ohneIban['id'], 'Ohne IBAN findet sich dasselbe Bankkonto über die Kontonummer' ); $this->assertEquals( 'DE53200411110847626900', (string)$ohneIban['iban'], 'Die einmal bekannte IBAN bleibt erhalten' ); $this->assertEquals( $this->giro, (int)$ohneIban['konto_id'], 'Und mit ihr die Zuordnung zum eigenen Konto' ); $this->assertEquals( 1, (int)Database::fetchValue( 'SELECT COUNT(*) FROM bank_konten WHERE zugang_id = ? AND kontonummer = ?', [$this->zugang, '847626900'], 0 ), 'Es gibt weiterhin nur einen Eintrag für dieses Konto' ); } /** Der umgekehrte Weg: erst ohne IBAN gesehen, dann mit. */ private function nachgelieferteIbanWirdNachgetragen(): void { $ohneIban = Bankzugang::findeOderLegeKontoAn($this->zugang, '', '847626905', 'Tagesgeld'); $this->assertEquals('', (string)$ohneIban['iban'], 'Beim ersten Sehen ohne IBAN angelegt'); $mitIban = Bankzugang::findeOderLegeKontoAn( $this->zugang, 'DE15200411110847626905', '847626905', 'Tagesgeld' ); $this->assertEquals((int)$ohneIban['id'], (int)$mitIban['id'], 'Es bleibt dasselbe Bankkonto'); $this->assertEquals( 'DE15200411110847626905', (string)Database::fetchValue('SELECT iban FROM bank_konten WHERE id = ?', [(int)$mitIban['id']], ''), 'Die nachgelieferte IBAN wurde eingetragen' ); } }