Merge pull request 'Fix #58: Auftragskonto über die Kontenzuordnung finden' (#59) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/59
This commit is contained in:
commit
26ad7dea6e
4 changed files with 147 additions and 8 deletions
13
Changes.md
13
Changes.md
|
|
@ -1,5 +1,18 @@
|
||||||
# Änderungen
|
# Ä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)
|
## Zahlungsverkehr: Namensabgleich vor der Überweisung (01.09.2026)
|
||||||
|
|
||||||
Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem
|
Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,11 @@ Fehlgeschlagene tragen die Meldung der Bank daneben.
|
||||||
Ohne Ausführungstermin geht die Überweisung sofort hinaus, mit Termin als
|
Ohne Ausführungstermin geht die Überweisung sofort hinaus, mit Termin als
|
||||||
Terminüberweisung; sie erscheint dann im Finanzkalender.
|
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
|
### SEPA-XML von Hand
|
||||||
|
|
||||||
php-fints erwartet die fertige **pain.001**-Nachricht und erzeugt sie nicht. Die üblichen
|
php-fints erwartet die fertige **pain.001**-Nachricht und erzeugt sie nicht. Die üblichen
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,11 @@ class FinTsUeberweisung
|
||||||
return $this->tanNoetig($fints, $konten, 'Kontenabfrage', 'konten');
|
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) {
|
if ($sepa === null) {
|
||||||
return $this->fehler(
|
return $this->fehler(
|
||||||
'Die Bank führt kein Konto mit der IBAN des Auftragskontos. '
|
'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);
|
$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) {
|
foreach ($konten as $kandidat) {
|
||||||
if ($iban !== '' && strtoupper((string)$kandidat->getIban()) === $iban) {
|
if (in_array(self::normiert((string)$kandidat->getIban()), $ibans, true)) {
|
||||||
return $kandidat;
|
return $kandidat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ohne IBAN am Konto: Hat die Bank nur ein Konto, ist es eindeutig.
|
// Ohne IBAN – etwa bei PayPal oder einem Kreditkartenkonto – bleibt die
|
||||||
return $iban === '' && count($konten) === 1 ? $konten[0] : null;
|
// 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> */
|
/** @return array<string,mixed> */
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Fhp\Model\SEPAAccount;
|
||||||
use Finanzmanager\Models\Bankzugang;
|
use Finanzmanager\Models\Bankzugang;
|
||||||
use Finanzmanager\Models\Konto;
|
use Finanzmanager\Models\Konto;
|
||||||
use Finanzmanager\Models\Zahlungsauftrag;
|
use Finanzmanager\Models\Zahlungsauftrag;
|
||||||
|
use Finanzmanager\Services\FinTsUeberweisung;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatische Bankauswahl im Zahlungsverkehr.
|
* Automatische Bankauswahl im Zahlungsverkehr.
|
||||||
|
|
@ -31,6 +33,85 @@ class UeberweisungBankZuordnungTest extends TestCase
|
||||||
$this->zugeordnetesKontoStehtInDerZuordnung();
|
$this->zugeordnetesKontoStehtInDerZuordnung();
|
||||||
$this->einDeaktivierterZugangFaelltHeraus();
|
$this->einDeaktivierterZugangFaelltHeraus();
|
||||||
$this->derAuftragUebernimmtDieBankAutomatisch();
|
$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
|
private function stammdaten(): void
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue