mandantAnlegen(); // Das Verzeichnis gehört keinem Benutzer: Was die Instanz schon // eingelesen hat, stünde sonst in jeder Zählung mit drin. Der Rollback // am Ende des Tests bringt es zurück. Bankinstitut::leeren(); $this->importLiestDieEchteKopfzeile(); $this->importErsetztDenBestand(); $this->falscheDateiWirdAbgewiesen(); $this->sucheFindetUeberBlzNameUndOrt(); $this->zugangUebernimmtDieAdresseAusDemVerzeichnis(); } // ── Testdaten ──────────────────────────────────────────────────────────── /** * Ein Ausschnitt der echten Datei – mit allen Spalten, in ISO-8859-1 und * mit den beiden Stolpersteinen: der über zwei Zeilen laufende, in * Anführungszeichen gesetzte Institutsname und die doppelte Bankleitzahl. */ private function beispieldatei(): string { $kopf = 'Nr.;BLZ;BIC;Institut;Ort;RZ;Organisation;HBCI-Zugang DNS;' . 'HBCI- Zugang IP-Adresse;HBCI-Version;DDV;RDH-1;RDH-2;RDH-3;RDH-4;RDH-5;' . 'RDH-6;RDH-7;RDH-8;RDH-9;RDH-10;RAH-7;RAH-9;RAH-10;PIN/TAN-Zugang URL;Version;' . 'Datum letzte Änderung'; $zeilen = [ $kopf, '1525;50050201;HELADEF1822;Frankfurter Sparkasse;Frankfurt am Main;' . 'Finanz Informatik GmbH & Co. KG;DSGV;;;;;;;;;;;;;;;;;;' . 'https://banking-hs6.s-fints-pt-hs.de/fints30;FinTS V3.0;09.02.2022', '1526;50050222;HELADEF1822;1822direkt (PIN/TAN);Frankfurt am Main;' . 'Finanz Informatik GmbH & Co. KG;DSGV;;;;;;;;;;;;;;;;;;' . 'https://fints.1822direkt.com/fints/hbci;FinTS V3.0;09.02.2022', // Zwei Filialen derselben Bankleitzahl, identischer Zugangspunkt. '29;12070000;DEUTDEBB160;Deutsche Bank;Potsdam;eigenes Rechenzentrum;BdB;;;3.0;' . ';;;;;;;;;;;;;;https://fints.deutsche-bank.de/;FinTS V3.0;12.03.2026', '30;12070000;DEUTDEBB160;Deutsche Bank;Cottbus;eigenes Rechenzentrum;BdB;;;3.0;' . ';;;;;;;;;;;;;;https://fints.deutsche-bank.de/;FinTS V3.0;12.03.2026', // Ein Institutsname über zwei Zeilen, in Anführungszeichen. '622;28030300;FORTDEH4XXX;"Oldenburgische Landesbank AG', '(vormals W. Fortmann & Söhne)";Oldenburg (Oldb);eigenes Rechenzentrum;;' . 'hbci.olb.de;;3.0;;ja;;;;;;;;ja;ja;;;;https://hbci.olb.de;FinTS V3.0;29.09.2017', // Ohne PIN/TAN-Zugang – die Zeile zählt mit, taugt aber nicht als Vorlage. '1822;55033300;SCFBDE33XXX;Santander Bank;Mainz;eigenes Rechenzentrum;BdB;' . 'sec.santanderbank.de;193243184115;3.0;;ja;;;;;;;;;;;;;;;26.02.2014', // Fußnote ohne Bankleitzahl – sie wird übergangen. (Eine völlig // leere Zeile fiele schon in CsvTabelle heraus und käme hier nie an.) 'Angaben ohne Gewähr;;;;;;;;;;;;;;;;;;;;;;;;;;', ]; $utf8 = implode("\r\n", $zeilen) . "\r\n"; // Die echte Datei kommt in ISO-8859-1; genau das soll der Import // erkennen, ohne dass jemand sie vorher umwandelt. return (string)mb_convert_encoding($utf8, 'ISO-8859-1', 'UTF-8'); } // ── Import ─────────────────────────────────────────────────────────────── private function importLiestDieEchteKopfzeile(): void { $pfad = $this->tempPfad('.csv'); file_put_contents($pfad, $this->beispieldatei()); $probe = FintsInstituteImport::ausDatei($pfad)->ausfuehren(true); $this->assertEquals(7, $probe['gelesen'], 'Alle Datenzeilen wurden gelesen'); $this->assertEquals(6, $probe['uebernommen'], 'Sechs Zeilen nennen eine Bankleitzahl'); $this->assertEquals(1, $probe['ohne_blz'], 'Die Fußnote wurde übergangen'); $this->assertEquals(5, $probe['banken'], 'Fünf verschiedene Bankleitzahlen'); $this->assertEquals(5, $probe['mit_zugang'], 'Fünf nennen einen PIN/TAN-Zugangspunkt'); $this->assertEquals('2026-03-12', $probe['stand'], 'Jüngste Änderung ist der 12.03.2026'); $this->assertEquals(0, Bankinstitut::anzahl(), 'Der Trockenlauf hat nichts geschrieben'); FintsInstituteImport::ausDatei($pfad)->ausfuehren(false); $this->assertEquals(6, Bankinstitut::anzahl(), 'Der Import hat sechs Zeilen geschrieben'); $this->assertEquals(5, Bankinstitut::anzahlMitZugang(), 'Davon fünf mit Zugangspunkt'); $this->assertEquals('2026-03-12', Bankinstitut::juengsterStand(), 'Der Stand steht in der Tabelle'); $direkt = Bankinstitut::findeNachBlz('50050222'); $this->assertNotEmpty($direkt, '1822direkt wurde gefunden'); $this->assertEquals( 'https://fints.1822direkt.com/fints/hbci', (string)$direkt['pintan_url'], 'Die FinTS-Adresse der 1822direkt stimmt' ); $this->assertEquals('HELADEF1822', (string)$direkt['bic'], 'Der BIC kam mit'); $this->assertEquals('Frankfurt am Main', (string)$direkt['ort'], 'Der Ort kam mit'); $this->assertEquals('FinTS V3.0', (string)$direkt['pintan_version'], 'Die Version kam mit'); // ISO-8859-1 und der Zeilenumbruch im Anführungszeichenfeld. $olb = Bankinstitut::findeNachBlz('28030300'); $this->assertNotEmpty($olb, 'Die Zeile mit dem Umbruch im Namen wurde gelesen'); $this->assert( str_contains((string)$olb['institut'], 'Söhne'), 'Der Umlaut aus ISO-8859-1 kam als „Söhne“ an' ); // Bei mehreren Filialen entscheidet die Reihenfolge der Datei. $deutsche = Bankinstitut::findeNachBlz('12070000'); $this->assertEquals('Potsdam', (string)$deutsche['ort'], 'Die erste Filialzeile gewinnt'); // Führende Nullen und Leerzeichen dürfen in der Anfrage stehen. $this->assertNotEmpty( Bankinstitut::findeNachBlz('500 502 22'), 'Eine Bankleitzahl mit Leerzeichen wird trotzdem gefunden' ); $this->assertNull( Bankinstitut::findeNachBlz(''), 'Eine leere Bankleitzahl liefert keinen Treffer' ); // Kein Zugangspunkt heißt: leeres Feld, nicht „nicht unterstützt“. $santander = Bankinstitut::findeNachBlz('55033300'); $this->assertEquals('', (string)$santander['pintan_url'], 'Ohne Zugang bleibt die Adresse leer'); } /** Ein zweiter Import ersetzt den Bestand, statt ihn zu verdoppeln. */ private function importErsetztDenBestand(): void { $pfad = $this->tempPfad('.csv'); file_put_contents($pfad, $this->beispieldatei()); FintsInstituteImport::ausDatei($pfad)->ausfuehren(false); $this->assertEquals(6, Bankinstitut::anzahl(), 'Nach dem zweiten Import stehen keine Dubletten da'); $kurz = "Nr.;BLZ;BIC;Institut;Ort;PIN/TAN-Zugang URL\r\n" . "1;10010010;PBNKDEFFXXX;Postbank;Berlin;https://hbci.postbank.de/banking/hbci.do\r\n"; FintsInstituteImport::ausText($kurz)->ausfuehren(false); $this->assertEquals(1, Bankinstitut::anzahl(), 'Der neue Bestand hat den alten abgelöst'); $this->assertNull(Bankinstitut::findeNachBlz('50050222'), 'Die alten Zeilen sind fort'); $this->assertNotEmpty(Bankinstitut::findeNachBlz('10010010'), 'Die neue Zeile steht da'); $this->assertNull( Bankinstitut::juengsterStand(), 'Ohne Datumsspalte bleibt der Stand offen, statt zu raten' ); // Für die folgenden Prüfungen wieder die volle Liste. FintsInstituteImport::ausDatei($pfad)->ausfuehren(false); } private function falscheDateiWirdAbgewiesen(): void { $this->assert( $this->wirftFehler(fn() => FintsInstituteImport::ausText( "Datum;Betrag;Empfänger\r\n01.02.2026;-12,50;Bäcker\r\n" )), 'Ein Kontoauszug ist keine Institutsliste' ); $this->assert( $this->wirftFehler(fn() => FintsInstituteImport::ausText('')), 'Eine leere Datei wird abgewiesen' ); $this->assert( $this->wirftFehler(fn() => FintsInstituteImport::ausText( "Nr.;BLZ;Institut;PIN/TAN-Zugang URL\r\n;;;\r\n" )), 'Eine Datei ohne eine einzige Bankleitzahl wird abgewiesen' ); $this->assertEquals(6, Bankinstitut::anzahl(), 'Der Bestand blieb dabei unangetastet'); } // ── Suche ──────────────────────────────────────────────────────────────── private function sucheFindetUeberBlzNameUndOrt(): void { $this->assertEquals(0, count(Bankinstitut::suche('')), 'Eine leere Suche liefert nichts'); // Ziffern gelten als Bankleitzahl und werden von vorn verglichen. $praefix = Bankinstitut::suche('5005'); $this->assertEquals(2, count($praefix), 'Zwei Frankfurter Leitzahlen beginnen mit 5005'); $nachName = Bankinstitut::suche('1822direkt'); $this->assertEquals(1, count($nachName), 'Der Name führt zu genau einem Treffer'); $this->assertEquals('50050222', (string)$nachName[0]['blz'], 'Und zwar zur richtigen Leitzahl'); $nachOrt = Bankinstitut::suche('Frankfurt'); $this->assertEquals(2, count($nachOrt), 'Der Ort findet beide Frankfurter Institute'); $nachBic = Bankinstitut::suche('DEUTDEBB160'); $this->assertEquals(2, count($nachBic), 'Der BIC findet beide Filialzeilen'); // Eine Ziffernfolge wird nicht nur als Bankleitzahl gelesen: „1822“ // steht bei der 1822direkt im Namen und bei der Frankfurter Sparkasse // im BIC – in keiner der beiden Bankleitzahlen. $gemischt = array_column(Bankinstitut::suche('1822'), 'blz'); $this->assertEquals(2, count($gemischt), '„1822“ findet beide über Name und BIC'); $this->assert( in_array('50050222', $gemischt, true), 'Darunter die 1822direkt, deren Leitzahl ganz anders anfängt' ); // Umgekehrt geht die Bankleitzahl vor: Ein Treffer in ihr steht oben. $this->assertEquals( '12070000', (string)Bankinstitut::suche('1207')[0]['blz'], 'Ein Treffer in der Bankleitzahl steht vor den Texttreffern' ); // Die Platzhalter von LIKE dürfen in der Eingabe stehen, ohne zu wirken. $this->assertEquals( 0, count(Bankinstitut::suche('%')), 'Ein Prozentzeichen sucht nach einem Prozentzeichen, nicht nach allem' ); } // ── Zusammenspiel mit der Bankverbindung ───────────────────────────────── private function zugangUebernimmtDieAdresseAusDemVerzeichnis(): void { $institut = Bankinstitut::findeNachBlz('50050222'); $id = Bankzugang::anlegen([ 'name' => '1822direkt', 'bankname' => (string)$institut['institut'], 'blz' => (string)$institut['blz'], 'fints_url' => (string)$institut['pintan_url'], 'benutzerkennung' => 'testkunde', 'pin' => 'geheim1234', 'abrufzeitraum' => 'letzter_abruf', 'aktiv' => 1, ]); $zugang = Bankzugang::find($id); $this->assertEquals( 'https://fints.1822direkt.com/fints/hbci', (string)$zugang['fints_url'], 'Die Adresse aus dem Verzeichnis steht im Zugang' ); $this->assertEquals('50050222', (string)$zugang['blz'], 'Die Bankleitzahl steht ohne Leerzeichen da'); // Ohne Adresse gibt es keinen FinTS-Zugang – der Controller schlägt sie // vorher nach, und findet auch er nichts, endet es hier mit einer // Meldung statt mit einem Zugang, der niemanden fragen kann. $this->assert( $this->wirftFehler(fn() => Bankzugang::anlegen([ 'name' => 'Ohne Adresse', 'blz' => '99999999', 'fints_url' => '', 'benutzerkennung' => 'testkunde', 'pin' => 'geheim1234', 'aktiv' => 1, ])), 'Ein FinTS-Zugang ohne Adresse wird abgewiesen' ); // PayPal führt keine FinTS-Adresse – dort darf sie leer bleiben. $paypal = Bankzugang::anlegen([ 'typ' => 'paypal', 'name' => 'PayPal-Verzeichnisprobe', 'api_benutzer' => 'api_test', 'api_kennwort' => 'geheim', 'api_signatur' => 'signatur', 'aktiv' => 1, ]); $this->assertEquals( '', (string)Bankzugang::find($paypal)['fints_url'], 'Ein PayPal-Zugang kommt ohne FinTS-Adresse aus' ); } /** Ruft $fn auf und meldet, ob dabei eine Ausnahme flog. */ private function wirftFehler(callable $fn): bool { try { $fn(); } catch (\Throwable) { return true; } return false; } }