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