From e7c965c2195d9769fb41e6cfba5ca23cb7b474a8 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 1 Sep 2026 20:08:54 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#58:=20Auftragskonto=20=C3=BCber=20die=20?= =?UTF-8?q?Kontenzuordnung=20finden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Senden suchte FinTsUeberweisung unter den Konten, die die Bank meldet, nach der IBAN aus dem Konto selbst. Die ist in der Kontenverwaltung als optional ausgewiesen und bleibt meist leer – dann scheiterte die Überweisung an "Die Bank führt kein Konto mit der IBAN des Auftragskontos", obwohl der Umsatzabruf für dasselbe Konto längst lief. Maßgeblich ist jetzt dieselbe Zuordnung wie beim Abruf: die Zeile in bank_konten, die dem Konto zugewiesen ist. Dort steht die IBAN, die die Bank selbst gemeldet hat, und die Kontonummer für die Häuser, die keine IBAN liefern. Die IBAN am Konto bleibt als Weg bestehen, ebenso der Fall, dass die Bank nur ein einziges Konto führt. passendesKonto() ist dafür public static geworden und nimmt den Zugang als Parameter – so lässt sich die Wahl mit echten SEPAAccount-Objekten prüfen, ohne eine Bank zu brauchen. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013iURiWYZJQYVGQNXFFGyWu --- Changes.md | 13 ++++ docs/nebenmodule.md | 5 ++ src/Services/FinTsUeberweisung.php | 56 ++++++++++++--- tests/37_UeberweisungBankZuordnungTest.php | 81 ++++++++++++++++++++++ 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/Changes.md b/Changes.md index e04fc31..43d0d2d 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,18 @@ # Änderungen +## Zahlungsverkehr: Auftragskonto über die Kontenzuordnung finden (01.09.2026) + +Beim Senden suchte `FinTsUeberweisung` unter den Konten, die die Bank meldet, nach der IBAN +aus dem Konto selbst. Die ist in der Kontenverwaltung aber als optional ausgewiesen und +bleibt meist leer – dann scheiterte die Überweisung an „Die Bank führt kein Konto mit der +IBAN des Auftragskontos“, obwohl der Umsatzabruf für dasselbe Konto längst lief. + +Maßgeblich ist jetzt dieselbe Zuordnung wie beim Abruf: die Zeile in `bank_konten`, die der +Nutzer dem Konto zugewiesen hat. Dort steht die IBAN, die die Bank selbst gemeldet hat, und +die Kontonummer für die Häuser, die keine IBAN liefern. Der bisherige Weg bleibt bestehen: +Wer die IBAN am Konto gepflegt hat, kommt auch ohne Zuordnung zum Ziel, und führt die Bank +nur ein einziges Konto, ist die Sache ohnehin eindeutig. + ## Zahlungsverkehr: Namensabgleich vor der Überweisung (01.09.2026) Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem diff --git a/docs/nebenmodule.md b/docs/nebenmodule.md index 7509111..92009e0 100644 --- a/docs/nebenmodule.md +++ b/docs/nebenmodule.md @@ -131,6 +131,11 @@ Fehlgeschlagene tragen die Meldung der Bank daneben. Ohne Ausführungstermin geht die Überweisung sofort hinaus, mit Termin als Terminüberweisung; sie erscheint dann im Finanzkalender. +**Welches Konto bei der Bank gemeint ist**, entscheidet dieselbe Zuordnung wie beim +Umsatzabruf: die Zeile in `bank_konten`, die dem Konto zugewiesen ist (IBAN, ersatzweise +Kontonummer). Die IBAN am Konto selbst ist optional und wird nur herangezogen, wenn keine +Zuordnung besteht – sonst müsste man dieselbe Angabe zweimal pflegen. + ### SEPA-XML von Hand php-fints erwartet die fertige **pain.001**-Nachricht und erzeugt sie nicht. Die üblichen diff --git a/src/Services/FinTsUeberweisung.php b/src/Services/FinTsUeberweisung.php index d8322af..a351906 100644 --- a/src/Services/FinTsUeberweisung.php +++ b/src/Services/FinTsUeberweisung.php @@ -232,7 +232,11 @@ class FinTsUeberweisung return $this->tanNoetig($fints, $konten, 'Kontenabfrage', 'konten'); } - $sepa = $this->passendesKonto($konten->getAccounts(), (int)$auftrag['konto_id']); + $sepa = self::passendesKonto( + $konten->getAccounts(), + (int)$auftrag['konto_id'], + (int)$this->zugang['id'] + ); if ($sepa === null) { return $this->fehler( 'Die Bank führt kein Konto mit der IBAN des Auftragskontos. ' @@ -375,21 +379,57 @@ class FinTsUeberweisung } /** - * @param list $konten + * Das Konto bei der Bank, das zum Auftragskonto gehört. + * + * Maßgeblich ist dieselbe Zuordnung, über die auch der Umsatzabruf läuft: + * die Zeile in `bank_konten`, die der Nutzer dem Konto zugewiesen hat. Dort + * steht die IBAN, die die Bank selbst gemeldet hat – und die Kontonummer für + * die Häuser, die keine IBAN liefern. Die IBAN am Konto selbst ist optional + * und bleibt in der Kontenverwaltung oft leer; wer sie gepflegt hat, kommt + * damit trotzdem zum Ziel. + * + * @param list $konten die Konten, die die Bank gemeldet hat */ - private function passendesKonto(array $konten, int $kontoId): ?SEPAAccount + public static function passendesKonto(array $konten, int $kontoId, int $zugangId): ?SEPAAccount { - $konto = Konto::find($kontoId); - $iban = strtoupper(str_replace(' ', '', (string)($konto['iban'] ?? ''))); + $ibans = []; + $kontonummern = []; + + foreach (Bankzugang::konten($zugangId) as $bankkonto) { + if ((int)($bankkonto['konto_id'] ?? 0) !== $kontoId) { + continue; + } + $ibans[] = self::normiert((string)($bankkonto['iban'] ?? '')); + $kontonummern[] = trim((string)($bankkonto['kontonummer'] ?? '')); + } + + $konto = Konto::find($kontoId); + $ibans[] = self::normiert((string)($konto['iban'] ?? '')); + + $ibans = array_values(array_filter($ibans)); + $kontonummern = array_values(array_filter($kontonummern)); foreach ($konten as $kandidat) { - if ($iban !== '' && strtoupper((string)$kandidat->getIban()) === $iban) { + if (in_array(self::normiert((string)$kandidat->getIban()), $ibans, true)) { return $kandidat; } } - // Ohne IBAN am Konto: Hat die Bank nur ein Konto, ist es eindeutig. - return $iban === '' && count($konten) === 1 ? $konten[0] : null; + // Ohne IBAN – etwa bei PayPal oder einem Kreditkartenkonto – bleibt die + // Kontonummer. + foreach ($konten as $kandidat) { + if ($kontonummern !== [] && in_array(trim((string)$kandidat->getAccountNumber()), $kontonummern, true)) { + return $kandidat; + } + } + + // Ist nichts zugeordnet und die Bank führt nur ein Konto, ist es eindeutig. + return $ibans === [] && $kontonummern === [] && count($konten) === 1 ? $konten[0] : null; + } + + private static function normiert(string $iban): string + { + return strtoupper(str_replace(' ', '', trim($iban))); } /** @return array */ diff --git a/tests/37_UeberweisungBankZuordnungTest.php b/tests/37_UeberweisungBankZuordnungTest.php index cc4e7eb..ba105c8 100644 --- a/tests/37_UeberweisungBankZuordnungTest.php +++ b/tests/37_UeberweisungBankZuordnungTest.php @@ -2,9 +2,11 @@ declare(strict_types=1); +use Fhp\Model\SEPAAccount; use Finanzmanager\Models\Bankzugang; use Finanzmanager\Models\Konto; use Finanzmanager\Models\Zahlungsauftrag; +use Finanzmanager\Services\FinTsUeberweisung; /** * Automatische Bankauswahl im Zahlungsverkehr. @@ -31,6 +33,85 @@ class UeberweisungBankZuordnungTest extends TestCase $this->zugeordnetesKontoStehtInDerZuordnung(); $this->einDeaktivierterZugangFaelltHeraus(); $this->derAuftragUebernimmtDieBankAutomatisch(); + $this->dasAuftragskontoFindetSichUeberDieZuordnung(); + $this->ohneZuordnungGreiftDieIbanAmKonto(); + } + + /** + * Beim Senden muss unter den Konten, die die Bank meldet, das richtige + * gefunden werden. Maßgeblich ist dieselbe Zuordnung wie beim Umsatzabruf – + * die IBAN am Konto selbst ist optional und bleibt meist leer. + */ + private function dasAuftragskontoFindetSichUeberDieZuordnung(): void + { + $gemeldet = [ + $this->sepaKonto('DE29120300000000202052', '202052'), + $this->sepaKonto('DE02120300000000202051', '202051'), + ]; + + // Das Girokonto ist über `bank_konten` zugeordnet (siehe oben), hat aber + // selbst keine IBAN. + $treffer = FinTsUeberweisung::passendesKonto($gemeldet, $this->giro, $this->zugang); + $this->assertEquals( + 'DE29120300000000202052', + $treffer?->getIban(), + 'Das zugeordnete Bankkonto wird gefunden, obwohl am Konto keine IBAN steht' + ); + + // Ein Konto ohne Zuordnung darf keines der gemeldeten erwischen. + $this->assertNull( + FinTsUeberweisung::passendesKonto($gemeldet, $this->ohneBank, $this->zugang), + 'Ein nicht zugeordnetes Konto trifft auf nichts' + ); + + // Meldet die Bank keine IBAN, bleibt die Kontonummer. + $ohneIban = [$this->sepaKonto('', '202052'), $this->sepaKonto('', '999999')]; + $this->assertEquals( + '202052', + FinTsUeberweisung::passendesKonto($ohneIban, $this->giro, $this->zugang)?->getAccountNumber(), + 'Ohne IBAN führt die Kontonummer zum Ziel' + ); + } + + /** + * Der bisherige Weg bleibt bestehen: Wer die IBAN am Konto gepflegt hat, + * kommt auch ohne Zuordnung zum Ziel – und bei genau einem Konto bei der + * Bank ist die Sache ohnehin eindeutig. + */ + private function ohneZuordnungGreiftDieIbanAmKonto(): void + { + $eigenes = Konto::anlegen([ + 'name' => 'Zahlungsverkehr-Giro mit IBAN', 'kontoart' => 'bank', + 'iban' => 'DE02120300000000202051', 'anfangssaldo' => 0, 'aktiv' => 1, + ]); + + $gemeldet = [ + $this->sepaKonto('DE29120300000000202052', '202052'), + $this->sepaKonto('DE02120300000000202051', '202051'), + ]; + + $this->assertEquals( + 'DE02120300000000202051', + FinTsUeberweisung::passendesKonto($gemeldet, $eigenes, $this->zugang)?->getIban(), + 'Die IBAN am Konto genügt auch ohne Zuordnung' + ); + + $einzelnes = [$this->sepaKonto('DE29120300000000202052', '202052')]; + $this->assertNotEmpty( + FinTsUeberweisung::passendesKonto($einzelnes, $this->ohneBank, $this->zugang), + 'Führt die Bank nur ein Konto, ist es eindeutig' + ); + } + + private function sepaKonto(string $iban, string $kontonummer): SEPAAccount + { + $konto = new SEPAAccount(); + $konto->setIban($iban); + $konto->setBic('GENODEF1TES'); + $konto->setAccountNumber($kontonummer); + $konto->setBlz('12030000'); + + return $konto; } private function stammdaten(): void