Fix #58: Auftragskonto über die Kontenzuordnung finden

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iURiWYZJQYVGQNXFFGyWu
This commit is contained in:
fds2610-bot 2026-09-01 20:08:54 +02:00
parent e425c76975
commit e7c965c219
4 changed files with 147 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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<SEPAAccount> $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<SEPAAccount> $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
{
$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);
$iban = strtoupper(str_replace(' ', '', (string)($konto['iban'] ?? '')));
$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<string,mixed> */

View file

@ -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