Merge pull request 'Fix #56: Namensabgleich vor der Überweisung' (#57) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/57
This commit is contained in:
commit
e425c76975
12 changed files with 591 additions and 24 deletions
29
Changes.md
29
Changes.md
|
|
@ -1,5 +1,34 @@
|
|||
# Änderungen
|
||||
|
||||
## Zahlungsverkehr: Namensabgleich vor der Überweisung (01.09.2026)
|
||||
|
||||
Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem
|
||||
Kontoinhaber ab (Verification of Payee). Der Finanzmanager kannte diese Stufe nicht, die
|
||||
Überweisung blieb stehen.
|
||||
|
||||
`FinTsUeberweisung` behandelt den Ablauf jetzt vollständig: Auf das Ergebnis wird gewartet,
|
||||
solange die Bank es braucht; **stimmt der Name überein, bestätigt der Finanzmanager selbst**
|
||||
und geht direkt zur TAN. Weicht der Name ab, ist er nur nahezu gleich oder konnte die Bank
|
||||
nicht prüfen, kommt die neue Maske `zahlungsverkehr/vop.php` – mit dem Ergebnis in einem
|
||||
deutschen Satz, dem Aufklärungstext der Bank als Klartext (ihr HTML wird abgeräumt) und der
|
||||
Wahl zwischen *Trotzdem senden* und *Abbrechen*. Beim Abbrechen bleibt der Auftrag Entwurf.
|
||||
|
||||
Stimmt der Name nur **nahezu** überein, zeigt die Maske neben dem angegebenen Empfänger
|
||||
auch den bei der Bank hinterlegten Kontoinhaber – die Angabe, die die Rückfrage überhaupt
|
||||
beantwortbar macht. Weicht der Name ganz ab, nennt die Bank ihn nicht; das steht dann als
|
||||
Erklärung in der Maske.
|
||||
|
||||
Dazu gehören zwei Eingriffe im FinTS-Fork (`v4.1.3`): Meldet eine Bank die schrittweise
|
||||
Lieferung des Prüfberichts (`Art der Lieferung Payment Status Report` = `S`, etwa
|
||||
comdirect), lehnte php-fints jede Überweisung ab. Der Parameter beschreibt laut
|
||||
Spezifikation aber nur, wie die Bank den Bericht aufteilt, *wenn* sie ihn über mehrere
|
||||
Runden liefert – bei einer Einzelüberweisung passiert das nie. Der Fork prüft ihn deshalb
|
||||
erst, wenn tatsächlich eine Teillieferung eintrifft. Und der beim Close Match gemeldete
|
||||
Kontoinhaber wurde zwar eingelesen, aber nicht an die Anwendung durchgereicht; dafür gibt
|
||||
es jetzt `VopConfirmationRequest::getDifferingPayeeName()`. Beide Commits liegen zusätzlich
|
||||
auf eigenen Branches (`vop-stepwise`, `vop-close-match-name`), damit daraus PRs an das
|
||||
Upstream-Projekt werden können.
|
||||
|
||||
## Zahlungsverkehr: SEPA-Format der Bank (01.09.2026)
|
||||
|
||||
Die Überweisung wurde bisher immer als `pain.001.001.03` eingereicht. Banken, die nur
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
],
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"nemiah/php-fints": "4.1.1"
|
||||
"nemiah/php-fints": "4.1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
14
composer.lock
generated
14
composer.lock
generated
|
|
@ -4,20 +4,20 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c9c68471574e1ff2ea7b78c3d71cdfd3",
|
||||
"content-hash": "2ed8c4171d96c85182d0ca57fb9c0a87",
|
||||
"packages": [
|
||||
{
|
||||
"name": "nemiah/php-fints",
|
||||
"version": "v4.1.1",
|
||||
"version": "v4.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/fds2610-bot/phpFinTS.git",
|
||||
"reference": "6ebd8e0ec1287dac384d404ca6311ba8169ee545"
|
||||
"reference": "76e3c648e88c2e5608044595d5920ba3ffee70a5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://codeberg.org/api/v1/repos/fds2610-bot/phpFinTS/archive/6ebd8e0ec1287dac384d404ca6311ba8169ee545.zip",
|
||||
"reference": "6ebd8e0ec1287dac384d404ca6311ba8169ee545",
|
||||
"url": "https://codeberg.org/api/v1/repos/fds2610-bot/phpFinTS/archive/76e3c648e88c2e5608044595d5920ba3ffee70a5.zip",
|
||||
"reference": "76e3c648e88c2e5608044595d5920ba3ffee70a5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -67,10 +67,10 @@
|
|||
"description": "PHP Library for the protocols fints and hbci",
|
||||
"homepage": "https://codeberg.org/fds2610-bot/phpFinTS",
|
||||
"support": {
|
||||
"source": "https://codeberg.org/fds2610-bot/phpFinTS/tag/v4.1.1",
|
||||
"source": "https://codeberg.org/fds2610-bot/phpFinTS/tag/v4.1.3",
|
||||
"issues": "https://codeberg.org/fds2610-bot/phpFinTS/issues"
|
||||
},
|
||||
"time": "2026-08-11T18:48:38+02:00"
|
||||
"time": "2026-09-01T19:22:02+02:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ passende Zugangsdaten noch passende Konten hätte.
|
|||
|
||||
Verwendet wird `nemiah/php-fints`, aber nicht von Packagist, sondern als **Fork auf
|
||||
Codeberg** (`fds2610-bot/phpFinTS`, eingebunden über ein VCS-Repository in der
|
||||
`composer.json`, Version fest auf `4.1.1` gepinnt).
|
||||
`composer.json`, Version fest auf `4.1.3` gepinnt).
|
||||
|
||||
Im Fork ist `psr/log` **direkt eingebaut**: Die acht Interface-Dateien liegen unter
|
||||
`psr-log/Psr/Log/` und werden über einen eigenen Autoload-Eintrag geladen, der
|
||||
|
|
@ -30,6 +30,26 @@ Kontrolle.
|
|||
Beim Nachziehen eines neuen Upstream-Standes muss dieser Eingriff wiederholt werden; wie,
|
||||
steht in `psr-log/README.md` des Forks.
|
||||
|
||||
### Eigene Änderungen am Protokoll
|
||||
|
||||
Dazu kommt ein Eingriff am **Namensabgleich** (Verification of Payee): Meldet eine Bank in
|
||||
den Bankparametern die schrittweise Lieferung des Prüfberichts (`Art der Lieferung Payment
|
||||
Status Report` = `S`, etwa comdirect), lehnte php-fints jede Überweisung ab. Der Parameter
|
||||
beschreibt aber nur, wie die Bank den Bericht aufteilt, *wenn* sie ihn über mehrere Runden
|
||||
liefert – bei einer Einzelüberweisung passiert das nie. Der Fork prüft ihn deshalb erst,
|
||||
wenn tatsächlich eine Teillieferung eintrifft.
|
||||
|
||||
Dazu kommt ein zweiter: Stimmt der Empfängername nur **nahezu** überein (Close Match),
|
||||
nennt die Bank den bei ihr hinterlegten Namen als Entscheidungshilfe. php-fints las ihn
|
||||
zwar aus dem Segment, reichte ihn aber nicht an die Anwendung durch – ausgerechnet die
|
||||
Angabe, die die Rückfrage überhaupt beantwortbar macht. Sie steht jetzt als
|
||||
`VopConfirmationRequest::getDifferingPayeeName()` zur Verfügung.
|
||||
|
||||
Jeder Eingriff liegt als eigener Commit vor, zusätzlich auf einem eigenen Branch über
|
||||
`master` (ohne den psr/log-Commit), damit daraus je ein PR an das Upstream-Projekt werden
|
||||
kann: `vop-stepwise` und `vop-close-match-name`. Der Stand, den der Finanzmanager
|
||||
verwendet, ist der Branch `finanzmanager` mit dem Tag `v4.1.3`.
|
||||
|
||||
## Das Bankleitzahlen-Verzeichnis
|
||||
|
||||
Beim Anlegen einer FinTS-Verbindung fehlt in aller Regel genau eine Angabe: der
|
||||
|
|
|
|||
|
|
@ -133,10 +133,16 @@ Terminüberweisung; sie erscheint dann im Finanzkalender.
|
|||
|
||||
### SEPA-XML von Hand
|
||||
|
||||
php-fints erwartet die fertige **pain.001.001.03**-Nachricht und erzeugt sie nicht. Die
|
||||
üblichen Erzeuger wären zwei weitere Abhängigkeiten für ein Dokument von sechzig Zeilen –
|
||||
php-fints erwartet die fertige **pain.001**-Nachricht und erzeugt sie nicht. Die üblichen
|
||||
Erzeuger wären zwei weitere Abhängigkeiten für ein Dokument von sechzig Zeilen –
|
||||
`Services\SepaXml` schreibt es deshalb selbst.
|
||||
|
||||
**Welches Format, sagt die Bank.** Die einen nehmen das ISO-Format `pain.001.001.03`, die
|
||||
anderen nur das ältere DK-Format `pain.001.003.03` – und weisen alles andere schon beim
|
||||
Einreichen ab. Welche Formate eine Bank führt, steht in den Bankparametern (Segment
|
||||
HISPAS); `SepaXml::schemaWaehlen()` sucht daraus das passende aus, ISO bevorzugt. Für die
|
||||
Einzelüberweisung sind beide Fassungen im Aufbau gleich, es genügt der andere Namensraum.
|
||||
|
||||
Der **SEPA-Zeichenvorrat** ist eng: `a–z A–Z 0–9 / - ? : ( ) . , ' +` und Leerzeichen.
|
||||
Umlaute werden umgeschrieben (ä → ae), alles Übrige fällt weg. Ohne diese Umschrift weist
|
||||
die Bank den Auftrag zurück – und zwar erst nach dem Absenden.
|
||||
|
|
@ -144,6 +150,36 @@ die Bank den Auftrag zurück – und zwar erst nach dem Absenden.
|
|||
Der Verwendungszweck endet bei 140 Zeichen; das ist das SEPA-Maß, alles darüber würde die
|
||||
Bank ohnehin abschneiden.
|
||||
|
||||
### Namensabgleich (Verification of Payee)
|
||||
|
||||
Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem
|
||||
Kontoinhaber ab. Der Ablauf ist eine Stufe vor der TAN:
|
||||
|
||||
1. Der Auftrag geht zusammen mit dem Prüfauftrag (Segment HKVPP) hinaus.
|
||||
2. Braucht die Bank für den Abgleich einen Moment, wird nachgefragt, bis das Ergebnis da
|
||||
ist – wie lange sie braucht, sagt sie selbst.
|
||||
3. **Stimmt der Name überein, bestätigt der Finanzmanager selbst** und geht direkt zur
|
||||
TAN. Eine Rückfrage ohne Erkenntnis erspart das dem Nutzer.
|
||||
4. **Weicht der Name ab**, ist er nur nahezu gleich oder konnte die Bank nicht prüfen,
|
||||
kommt die Maske `zahlungsverkehr/vop.php`: das Ergebnis in einem deutschen Satz, der
|
||||
Aufklärungstext der Bank als Klartext (ihr HTML wird abgeräumt), Empfänger und IBAN aus
|
||||
dem Auftrag, und die Wahl zwischen *Trotzdem senden* und *Abbrechen*.
|
||||
|
||||
Stimmt der Name nur **nahezu** überein, nennt die Bank den bei ihr hinterlegten
|
||||
Kontoinhaber – er steht dann in der Maske neben dem angegebenen Namen. Weicht der Name
|
||||
ganz ab, bleibt er geheim: Sonst ließen sich über beliebige IBANs Kontoinhaber ausspähen.
|
||||
Die Spezifikation erlaubt die Angabe ausdrücklich nur beim Close Match.
|
||||
|
||||
Wer trotz Abweichung sendet, verliert den Erstattungsanspruch – deshalb steht das auch in
|
||||
der Maske. Beim Abbrechen bleibt der Auftrag als Entwurf stehen, der Empfängername lässt
|
||||
sich korrigieren und der Auftrag erneut senden.
|
||||
|
||||
Meldet die Bank die **schrittweise Lieferung** des Prüfberichts (BPD-Parameter „Art der
|
||||
Lieferung Payment Status Report" = `S`, etwa bei comdirect), betrifft das nur sehr große
|
||||
Sammler, bei denen die Bank den Bericht über mehrere Runden schickt. php-fints lehnte
|
||||
solche Banken bisher pauschal ab; der Fork auf Codeberg prüft das erst, wenn wirklich eine
|
||||
Teillieferung kommt (siehe `docs/banking.md`).
|
||||
|
||||
### TAN
|
||||
|
||||
Für Überweisungen verlangt jede Bank eine TAN. Der Dialog wird genauso über die Sitzung
|
||||
|
|
@ -161,3 +197,10 @@ IBAN-Prüfung mit gültigen und falschen Nummern, Kündigungsfristen und Jahresb
|
|||
Sparfortschritt aus dem Konto, Tilgungsplan (Annuität, letzte Rate, Sondertilgung,
|
||||
zu kleine Rate), Zahlungsaufträge (Buchung erst nach Annahme, Unveränderlichkeit,
|
||||
Eingabeprüfungen), SEPA-XML (Schema, Zeichensatz, Wohlgeformtheit) und der Finanzkalender.
|
||||
|
||||
`tests/39_SepaSchemaTest.php` – Formatwahl nach den Angaben der Bank; die erzeugte
|
||||
Nachricht wird gegen das beiliegende DK-Schema `tests/schemata/pain.001.003.03.xsd`
|
||||
validiert.
|
||||
|
||||
`tests/40_NamensabgleichTest.php` – deutsche Sätze zu jedem Prüfergebnis, Aufbereitung des
|
||||
Banktexts und die Absicherung, dass ohne wartenden Vorgang nichts bestätigt werden kann.
|
||||
|
|
|
|||
|
|
@ -251,6 +251,8 @@ $router->post('/zahlungsverkehr/neu', fn() => (new ZahlungsverkehrController())-
|
|||
$router->get('/zahlungsverkehr/tan', fn() => (new ZahlungsverkehrController())->tanFormular());
|
||||
$router->post('/zahlungsverkehr/tan', fn() => (new ZahlungsverkehrController())->tanEinreichen());
|
||||
$router->post('/zahlungsverkehr/tan/abbrechen', fn() => (new ZahlungsverkehrController())->tanAbbrechen());
|
||||
$router->get('/zahlungsverkehr/vop', fn() => (new ZahlungsverkehrController())->vopFormular());
|
||||
$router->post('/zahlungsverkehr/vop', fn() => (new ZahlungsverkehrController())->vopBestaetigen());
|
||||
|
||||
$router->get('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->bearbeiten($p));
|
||||
$router->post('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->speichern($p));
|
||||
|
|
|
|||
|
|
@ -184,6 +184,30 @@ class ZahlungsverkehrController extends BaseController
|
|||
$this->zeige(FinTsUeberweisung::tanEinreichen($this->post('tan')));
|
||||
}
|
||||
|
||||
public function vopFormular(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$vop = FinTsUeberweisung::offeneVopAnfrage();
|
||||
if ($vop === null) {
|
||||
View::redirect('/zahlungsverkehr', 'Es wartet kein Namensabgleich auf eine Bestätigung.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render('zahlungsverkehr/vop.php', [
|
||||
'titel' => 'Namensabgleich prüfen',
|
||||
'vop' => $vop,
|
||||
'meldung' => 'Die Bank meldet eine Abweichung beim Namensabgleich.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function vopBestaetigen(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$this->zeige(FinTsUeberweisung::vopBestaetigen());
|
||||
}
|
||||
|
||||
public function tanAbbrechen(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
|
@ -196,6 +220,15 @@ class ZahlungsverkehrController extends BaseController
|
|||
/** @param array<string,mixed> $ergebnis */
|
||||
private function zeige(array $ergebnis): void
|
||||
{
|
||||
if ($ergebnis['status'] === 'vop') {
|
||||
$this->render('zahlungsverkehr/vop.php', [
|
||||
'titel' => 'Namensabgleich prüfen',
|
||||
'vop' => $ergebnis['vop'] ?? [],
|
||||
'meldung' => $ergebnis['meldung'],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ergebnis['status'] === 'tan') {
|
||||
$this->render('zahlungsverkehr/tan.php', [
|
||||
'titel' => 'Überweisung freigeben',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use Fhp\BaseAction;
|
|||
use Fhp\FinTs;
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Model\TanRequestChallengeImage;
|
||||
use Fhp\Model\VopConfirmationRequest;
|
||||
use Fhp\Model\VopVerificationResult;
|
||||
use Fhp\Options\Credentials;
|
||||
use Fhp\Options\FinTsOptions;
|
||||
use Fhp\Segment\SPA\HISPAS;
|
||||
|
|
@ -31,6 +33,12 @@ use Finanzmanager\Models\Zahlungsauftrag;
|
|||
* entsteht erst, wenn die Bank den Auftrag angenommen hat. Vorher wäre sie eine
|
||||
* Behauptung – und eine, die man nach einer abgelehnten TAN wieder einsammeln
|
||||
* müsste.
|
||||
*
|
||||
* **Namensabgleich.** Seit Oktober 2025 gleicht die Bank vor der Ausführung den
|
||||
* Empfängernamen mit dem Kontoinhaber ab (Verification of Payee). Stimmt beides
|
||||
* überein, geht es ohne Rückfrage weiter zur TAN. Weicht der Name ab oder konnte
|
||||
* die Bank nicht prüfen, wird zuerst gefragt – mit dem Text, den die Bank dazu
|
||||
* mitliefert.
|
||||
*/
|
||||
class FinTsUeberweisung
|
||||
{
|
||||
|
|
@ -39,6 +47,11 @@ class FinTsUeberweisung
|
|||
private const PRODUKT = 'F399A6377D4D60E0A222D677A';
|
||||
private const PRODUKT_VERSION = '1.0';
|
||||
|
||||
/** So oft fragen wir die Bank nach dem Ergebnis des Namensabgleichs … */
|
||||
private const POLLING_VERSUCHE = 10;
|
||||
/** … und so lange warten wir dazwischen höchstens (Sekunden). */
|
||||
private const POLLING_WARTEZEIT = 6;
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $zugang mit entschlüsselter PIN
|
||||
* @param array<string,mixed> $zustand
|
||||
|
|
@ -53,7 +66,7 @@ class FinTsUeberweisung
|
|||
/**
|
||||
* Startet das Absenden.
|
||||
*
|
||||
* @return array{status: string, meldung: string, tan?: array<string,mixed>}
|
||||
* @return array{status: string, meldung: string, tan?: array<string,mixed>, vop?: array<string,mixed>}
|
||||
*/
|
||||
public static function senden(int $auftragId): array
|
||||
{
|
||||
|
|
@ -86,7 +99,7 @@ class FinTsUeberweisung
|
|||
/**
|
||||
* Setzt nach der TAN-Eingabe fort.
|
||||
*
|
||||
* @return array{status: string, meldung: string, tan?: array<string,mixed>}
|
||||
* @return array{status: string, meldung: string, tan?: array<string,mixed>, vop?: array<string,mixed>}
|
||||
*/
|
||||
public static function tanEinreichen(string $tan): array
|
||||
{
|
||||
|
|
@ -104,6 +117,37 @@ class FinTsUeberweisung
|
|||
return (new self($zugang, (int)$zustand['auftrag_id'], $zustand))->lauf(trim($tan));
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt fort, nachdem der Nutzer den abweichenden Namensabgleich bestätigt
|
||||
* hat: Die Bank bekommt die Freigabe und führt den Auftrag anschließend
|
||||
* genauso aus wie ohne Abweichung – die TAN kommt danach.
|
||||
*
|
||||
* @return array{status: string, meldung: string, tan?: array<string,mixed>, vop?: array<string,mixed>}
|
||||
*/
|
||||
public static function vopBestaetigen(): array
|
||||
{
|
||||
$zustand = self::zustandLesen();
|
||||
if ($zustand === null || ($zustand['schritt'] ?? '') !== 'vop') {
|
||||
return ['status' => 'fehler', 'meldung' => 'Es wartet kein Namensabgleich auf eine Bestätigung.'];
|
||||
}
|
||||
|
||||
$zugang = Bankzugang::zugangsdaten((int)$zustand['zugang_id']);
|
||||
if ($zugang === null) {
|
||||
self::vergessen();
|
||||
return ['status' => 'fehler', 'meldung' => 'Die Bankverbindung gibt es nicht mehr.'];
|
||||
}
|
||||
|
||||
return (new self($zugang, (int)$zustand['auftrag_id'], $zustand))->lauf(null, true);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public static function offeneVopAnfrage(): ?array
|
||||
{
|
||||
$zustand = self::zustandLesen();
|
||||
|
||||
return $zustand === null ? null : ($zustand['vop'] ?? null);
|
||||
}
|
||||
|
||||
public static function wartetAufTan(): bool
|
||||
{
|
||||
return self::zustandLesen() !== null;
|
||||
|
|
@ -125,7 +169,7 @@ class FinTsUeberweisung
|
|||
// ── Ablauf ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function lauf(?string $tan): array
|
||||
private function lauf(?string $tan, bool $vopBestaetigt = false): array
|
||||
{
|
||||
$auftrag = Zahlungsauftrag::find($this->auftragId);
|
||||
if ($auftrag === null) {
|
||||
|
|
@ -140,6 +184,18 @@ class FinTsUeberweisung
|
|||
}
|
||||
|
||||
try {
|
||||
// Der Nutzer hat den abweichenden Namensabgleich bestätigt.
|
||||
if ($vopBestaetigt) {
|
||||
$aktion = $this->wartendeAktion();
|
||||
if ($aktion === null) {
|
||||
return $this->fehler('Der wartende Vorgang ist nicht mehr lesbar.');
|
||||
}
|
||||
|
||||
$fints->confirmVop($aktion);
|
||||
|
||||
return $this->nachDemSenden($fints, $aktion, true);
|
||||
}
|
||||
|
||||
// Eine wartende Aktion zuerst abschließen.
|
||||
if ($tan !== null && isset($this->zustand['aktion'])) {
|
||||
$aktion = unserialize((string)$this->zustand['aktion'], ['allowed_classes' => true]);
|
||||
|
|
@ -154,7 +210,7 @@ class FinTsUeberweisung
|
|||
$fints->submitTan($aktion, $tan);
|
||||
|
||||
if ($this->zustand['schritt'] === 'senden') {
|
||||
return $this->angenommen($fints);
|
||||
return $this->nachDemSenden($fints, $aktion, true);
|
||||
}
|
||||
|
||||
$this->zustand['schritt'] = $this->zustand['schritt'] === 'anmelden' ? 'konten' : 'senden';
|
||||
|
|
@ -207,11 +263,7 @@ class FinTsUeberweisung
|
|||
$aktion = SendSEPATransfer::create($sepa, $xml);
|
||||
$fints->execute($aktion);
|
||||
|
||||
if ($aktion->needsTan()) {
|
||||
return $this->tanNoetig($fints, $aktion, 'Überweisung', 'senden');
|
||||
}
|
||||
|
||||
return $this->angenommen($fints);
|
||||
return $this->nachDemSenden($fints, $aktion);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
DebugLog::fehler($e);
|
||||
|
|
@ -243,6 +295,63 @@ class FinTsUeberweisung
|
|||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Was nach dem Einreichen des Auftrags noch zu tun ist: auf den
|
||||
* Namensabgleich warten, ihn bestätigen lassen, die TAN einholen.
|
||||
*
|
||||
* @param bool $vopErledigt Ob die VOP-Bestätigung schon gegeben wurde – sie
|
||||
* darf nicht zweimal automatisch erfolgen.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function nachDemSenden(FinTs $fints, BaseAction $aktion, bool $vopErledigt = false): array
|
||||
{
|
||||
// Der Namensabgleich läuft bei der Bank; sie nennt selbst, wie lange sie
|
||||
// dafür braucht. Nach einer Minute geben wir auf – der Auftrag bleibt
|
||||
// dann Entwurf und lässt sich erneut senden.
|
||||
for ($versuch = 0; $aktion->needsPollingWait() && $versuch < self::POLLING_VERSUCHE; $versuch++) {
|
||||
$wartezeit = $aktion->getPollingInfo()?->getNextAttemptInSeconds() ?? 2;
|
||||
sleep(max(1, min($wartezeit, self::POLLING_WARTEZEIT)));
|
||||
$fints->pollAction($aktion);
|
||||
}
|
||||
|
||||
if ($aktion->needsPollingWait()) {
|
||||
return $this->fehler('Die Bank hat den Namensabgleich nicht rechtzeitig abgeschlossen.');
|
||||
}
|
||||
|
||||
if ($aktion->needsVopConfirmation()) {
|
||||
$anfrage = $aktion->getVopConfirmationRequest();
|
||||
|
||||
// Stimmt der Name, ist die Rückfrage nur ein Klick ohne Erkenntnis –
|
||||
// die Bank will die Bestätigung trotzdem sehen.
|
||||
if (!$vopErledigt && $anfrage?->getVerificationResult() === VopVerificationResult::CompletedFullMatch) {
|
||||
DebugLog::schreibe('fints', 'Namensabgleich ohne Abweichung – automatisch bestätigt.');
|
||||
$fints->confirmVop($aktion);
|
||||
|
||||
return $this->nachDemSenden($fints, $aktion, true);
|
||||
}
|
||||
|
||||
if ($anfrage !== null) {
|
||||
return $this->vopNoetig($fints, $aktion, $anfrage);
|
||||
}
|
||||
}
|
||||
|
||||
if ($aktion->needsTan()) {
|
||||
return $this->tanNoetig($fints, $aktion, 'Überweisung', 'senden');
|
||||
}
|
||||
|
||||
return $this->angenommen($fints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Vorgang, der in der Sitzung auf die Fortsetzung wartet.
|
||||
*/
|
||||
private function wartendeAktion(): ?BaseAction
|
||||
{
|
||||
$aktion = unserialize((string)($this->zustand['aktion'] ?? ''), ['allowed_classes' => true]);
|
||||
|
||||
return $aktion instanceof BaseAction ? $aktion : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $auftrag
|
||||
* @return array<string,mixed>
|
||||
|
|
@ -298,6 +407,92 @@ class FinTsUeberweisung
|
|||
return ['status' => 'erfolg', 'meldung' => 'Die Überweisung wurde an die Bank übergeben.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Namensabgleich weicht ab – der Nutzer muss entscheiden.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function vopNoetig(FinTs $fints, BaseAction $aktion, VopConfirmationRequest $anfrage): array
|
||||
{
|
||||
$auftrag = Zahlungsauftrag::find($this->auftragId);
|
||||
|
||||
$vop = [
|
||||
'ergebnis' => (string)($anfrage->getVerificationResult() ?? ''),
|
||||
'urteil' => self::vopUrteil($anfrage->getVerificationResult()),
|
||||
'grund' => (string)($anfrage->getVerificationNotApplicableReason() ?? ''),
|
||||
'text' => self::vopHinweis((string)($anfrage->getInformationForUser() ?? '')),
|
||||
'gueltig_bis' => $anfrage->getExpiration()?->format('d.m.Y H:i'),
|
||||
// Den hinterlegten Kontoinhaber nennt die Bank nur, wenn der Name
|
||||
// nahezu übereinstimmt – als Entscheidungshilfe. Weicht er ganz ab,
|
||||
// bleibt er geheim; sonst ließen sich über beliebige IBANs
|
||||
// Kontoinhaber ausspähen.
|
||||
'hinterlegt' => (string)($anfrage->getDifferingPayeeName() ?? ''),
|
||||
'empfaenger' => (string)($auftrag['empfaenger'] ?? ''),
|
||||
'iban' => (string)($auftrag['iban'] ?? ''),
|
||||
];
|
||||
|
||||
DebugLog::schreibe('fints', 'Namensabgleich mit Abweichung: ' . $vop['ergebnis']);
|
||||
|
||||
self::zustandSchreiben([
|
||||
'schritt' => 'vop',
|
||||
'aktion' => serialize($aktion),
|
||||
'dialog' => $fints->persist(),
|
||||
'sepa' => $this->zustand['sepa'] ?? null,
|
||||
'vop' => $vop,
|
||||
'zugang_id' => (int)$this->zugang['id'],
|
||||
'auftrag_id' => $this->auftragId,
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'vop',
|
||||
'meldung' => 'Die Bank meldet eine Abweichung beim Namensabgleich.',
|
||||
'vop' => $vop,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Hinweistext der Bank als Klartext.
|
||||
*
|
||||
* Die Banken liefern hier HTML. Übernommen werden nur die Absätze als
|
||||
* Zeilenumbrüche; die Auszeichnung fliegt weg – fremdes Markup gehört nicht
|
||||
* ungeprüft in die eigene Seite.
|
||||
*/
|
||||
public static function vopHinweis(string $html): string
|
||||
{
|
||||
if (trim($html) === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = preg_replace('#<(br|/p|/div|/li|/h[1-6])[^>]*>#i', "\n", $html) ?? $html;
|
||||
$text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$text = preg_replace('/[ \t]+/', ' ', $text) ?? $text;
|
||||
$text = preg_replace("/\n{3,}/", "\n\n", $text) ?? $text;
|
||||
|
||||
return trim(implode("\n", array_map('trim', explode("\n", $text))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Prüfergebnis der Bank in einem Satz – die englischen Codes der
|
||||
* Bibliothek sagen der Partnerin nichts.
|
||||
*/
|
||||
public static function vopUrteil(?string $ergebnis): string
|
||||
{
|
||||
return match ($ergebnis) {
|
||||
VopVerificationResult::CompletedFullMatch =>
|
||||
'Der Empfängername stimmt mit dem Kontoinhaber überein.',
|
||||
VopVerificationResult::CompletedCloseMatch =>
|
||||
'Der Empfängername stimmt nur nahezu mit dem Kontoinhaber überein.',
|
||||
VopVerificationResult::CompletedNoMatch =>
|
||||
'Der Empfängername stimmt nicht mit dem Kontoinhaber überein.',
|
||||
VopVerificationResult::CompletedPartialMatch =>
|
||||
'Nicht jeder Empfänger konnte bestätigt werden.',
|
||||
VopVerificationResult::NotApplicable =>
|
||||
'Die Bank konnte den Empfängernamen nicht prüfen.',
|
||||
default =>
|
||||
'Die Bank hat kein eindeutiges Ergebnis zum Empfängernamen geliefert.',
|
||||
};
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function tanNoetig(FinTs $fints, BaseAction $aktion, string $bezeichnung, string $schritt): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,18 @@ class SepaXml
|
|||
/** Die Formate, die hier erzeugt werden – in der Reihenfolge des Vorzugs. */
|
||||
private const SCHEMATA = [self::SCHEMA_PAIN_001_001_03, self::SCHEMA_PAIN_001_003_03];
|
||||
|
||||
/**
|
||||
* Das Ausführungsdatum für „sofort“.
|
||||
*
|
||||
* Das Format verlangt ein Datum, auch wenn keins gewünscht ist. Die Deutsche
|
||||
* Kreditwirtschaft hat dafür den 01.01.1999 vorgesehen; erst daran erkennt
|
||||
* die Bank eine gewöhnliche Überweisung (HKCCS). Steht dort ein echtes
|
||||
* Datum – und sei es das von heute –, wird daraus eine terminierte
|
||||
* Überweisung (HKCSE), und die weist die Bank mit „Die minimale Vorlaufzeit
|
||||
* beträgt 1 Tage“ zurück.
|
||||
*/
|
||||
public const SOFORT = '1999-01-01';
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* absender_name: string, absender_iban: string, absender_bic?: string,
|
||||
|
|
@ -48,9 +60,7 @@ class SepaXml
|
|||
$kennung = 'FM' . date('YmdHis') . substr(bin2hex(random_bytes(4)), 0, 6);
|
||||
$zeitpunkt = date('Y-m-d\TH:i:s');
|
||||
|
||||
// Ohne Termin führt die Bank sofort aus. Der Wert muss trotzdem
|
||||
// dastehen; das Format verlangt ihn.
|
||||
$ausfuehrung = (string)($auftrag['ausfuehrung_am'] ?? '') ?: date('Y-m-d');
|
||||
$ausfuehrung = self::ausfuehrungstag((string)($auftrag['ausfuehrung_am'] ?? ''));
|
||||
|
||||
$waehrung = strtoupper((string)($auftrag['waehrung'] ?? 'EUR'));
|
||||
$zweck = self::text((string)($auftrag['verwendungszweck'] ?? ''), 140);
|
||||
|
|
@ -159,6 +169,22 @@ class SepaXml
|
|||
return (string)$dokument->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Tag, an dem die Bank ausführen soll.
|
||||
*
|
||||
* Ohne Termin – und ebenso bei einem Termin, der heute oder schon vorbei ist –
|
||||
* ist „sofort“ gemeint: Dann steht das Platzhalterdatum `SOFORT` in der
|
||||
* Nachricht. Ein Termin in der Zukunft bleibt stehen und macht daraus eine
|
||||
* terminierte Überweisung.
|
||||
*/
|
||||
public static function ausfuehrungstag(string $termin, ?string $heute = null): string
|
||||
{
|
||||
$termin = trim($termin);
|
||||
$heute = $heute ?? date('Y-m-d');
|
||||
|
||||
return $termin === '' || $termin <= $heute ? self::SOFORT : $termin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Format, das zu den von der Bank gemeldeten SEPA-Datenformaten passt.
|
||||
*
|
||||
|
|
|
|||
90
src/Views/zahlungsverkehr/vop.php
Normal file
90
src/Views/zahlungsverkehr/vop.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
use Finanzmanager\View;
|
||||
|
||||
/**
|
||||
* Der Namensabgleich der Bank weicht ab (Verification of Payee).
|
||||
*
|
||||
* Die Bank hat den Empfängernamen mit dem Kontoinhaber verglichen und meldet
|
||||
* eine Abweichung. Hier steht die Entscheidung: trotzdem senden oder abbrechen
|
||||
* und den Empfänger korrigieren. Der Text der Bank wird wortgleich
|
||||
* wiedergegeben – ohne die Auszeichnungen, die sie mitschickt.
|
||||
*
|
||||
* @var array<string,mixed> $vop
|
||||
* @var string $meldung
|
||||
* @var string $csrfToken
|
||||
*/
|
||||
|
||||
// Der Text kommt bereits als Klartext aus `FinTsUeberweisung::vopHinweis()`.
|
||||
$banktext = (string)($vop['text'] ?? '');
|
||||
|
||||
include __DIR__ . '/../layout/kopf.php';
|
||||
?>
|
||||
|
||||
<div class="kachel einzeln">
|
||||
<header>
|
||||
<h2>Namensabgleich prüfen</h2>
|
||||
<p class="kachelhinweis"><?= View::h($vop['urteil'] ?? '') ?></p>
|
||||
</header>
|
||||
|
||||
<p class="hinweis"><?= View::h($meldung) ?></p>
|
||||
|
||||
<dl class="merkmale">
|
||||
<?php if (($vop['empfaenger'] ?? '') !== ''): ?>
|
||||
<dt>So lautet der Empfänger im Auftrag</dt>
|
||||
<dd><?= View::h($vop['empfaenger']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($vop['hinterlegt'] ?? '') !== ''): ?>
|
||||
<dt>So lautet er bei der Bank des Empfängers</dt>
|
||||
<dd><?= View::h($vop['hinterlegt']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($vop['iban'] ?? '') !== ''): ?>
|
||||
<dt>Empfänger-IBAN</dt>
|
||||
<dd><?= View::h($vop['iban']) ?></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
|
||||
<?php if (($vop['hinterlegt'] ?? '') === ''): ?>
|
||||
<p class="feldhinweis">
|
||||
Den Namen, der bei der Bank des Empfängers hinterlegt ist, nennt die Bank nur,
|
||||
wenn er dem angegebenen nahezu entspricht. Weicht er ganz ab, bleibt er geheim –
|
||||
sonst ließen sich über beliebige IBANs Kontoinhaber ausspähen.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($vop['grund'] ?? '') !== ''): ?>
|
||||
<p class="feldhinweis">Begründung der Bank: <?= View::h($vop['grund']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($banktext !== ''): ?>
|
||||
<p class="tanhinweis"><?= nl2br(View::h($banktext)) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($vop['gueltig_bis'] ?? null) !== null): ?>
|
||||
<p class="feldhinweis">Die Bank hält das Ergebnis bis <?= View::h($vop['gueltig_bis']) ?> vor.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/zahlungsverkehr/vop" class="formular">
|
||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf primaer">Trotzdem senden</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/zahlungsverkehr/tan/abbrechen" class="formular">
|
||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||
<div class="knopfleiste">
|
||||
<button type="submit" class="knopf">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="feldhinweis">
|
||||
Beim Abbrechen bleibt der Auftrag als Entwurf stehen; der Empfängername lässt sich
|
||||
dann korrigieren und der Auftrag erneut senden. Wird trotz Abweichung gesendet,
|
||||
haftet die Bank nicht, wenn das Geld beim falschen Empfänger landet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||
|
|
@ -28,6 +28,49 @@ class SepaSchemaTest extends TestCase
|
|||
$this->unbekannteFormateFuehrenZuNull();
|
||||
$this->dkFormatIstSchemakonform();
|
||||
$this->dasFormatStehtImNamensraum();
|
||||
$this->ohneTerminWirdSofortUeberwiesen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ohne Termin muss das Platzhalterdatum der Deutschen Kreditwirtschaft in
|
||||
* der Nachricht stehen. Steht dort ein echtes Datum, macht die Bibliothek
|
||||
* daraus eine terminierte Überweisung (HKCSE statt HKCCS) – und die Bank
|
||||
* weist sie zurück: „Die minimale Vorlaufzeit beträgt 1 Tage.“
|
||||
*/
|
||||
private function ohneTerminWirdSofortUeberwiesen(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
SepaXml::SOFORT,
|
||||
SepaXml::ausfuehrungstag('', '2026-09-01'),
|
||||
'Ohne Termin gilt sofort'
|
||||
);
|
||||
$this->assertEquals(
|
||||
SepaXml::SOFORT,
|
||||
SepaXml::ausfuehrungstag('2026-09-01', '2026-09-01'),
|
||||
'Der heutige Tag ist ebenfalls sofort – sonst verlangt die Bank Vorlaufzeit'
|
||||
);
|
||||
$this->assertEquals(
|
||||
SepaXml::SOFORT,
|
||||
SepaXml::ausfuehrungstag('2026-08-30', '2026-09-01'),
|
||||
'Ein verstrichener Termin auch'
|
||||
);
|
||||
$this->assertEquals(
|
||||
'2026-09-15',
|
||||
SepaXml::ausfuehrungstag('2026-09-15', '2026-09-01'),
|
||||
'Ein Termin in der Zukunft bleibt stehen'
|
||||
);
|
||||
|
||||
$auftrag = $this->auftraege()['ohne BIC'];
|
||||
$this->assert(
|
||||
str_contains(SepaXml::ueberweisung($auftrag), '<ReqdExctnDt>' . SepaXml::SOFORT . '</ReqdExctnDt>'),
|
||||
'Und das Platzhalterdatum steht auch wirklich in der Nachricht'
|
||||
);
|
||||
|
||||
$auftrag['ausfuehrung_am'] = date('Y-m-d', strtotime('+30 days'));
|
||||
$this->assert(
|
||||
str_contains(SepaXml::ueberweisung($auftrag), '<ReqdExctnDt>' . $auftrag['ausfuehrung_am'] . '</ReqdExctnDt>'),
|
||||
'Ein Termin in der Zukunft steht unverändert drin'
|
||||
);
|
||||
}
|
||||
|
||||
private function schemaFolgtDenAngabenDerBank(): void
|
||||
|
|
@ -131,7 +174,7 @@ class SepaSchemaTest extends TestCase
|
|||
'betrag' => 123.45,
|
||||
'waehrung' => 'EUR',
|
||||
'verwendungszweck' => 'Abschlag Strom & Wasser 09/2026',
|
||||
'ausfuehrung_am' => '2026-09-05',
|
||||
'ausfuehrung_am' => '2099-09-05',
|
||||
],
|
||||
'ohne BIC' => [
|
||||
'absender_name' => 'Max Mustermann',
|
||||
|
|
|
|||
86
tests/40_NamensabgleichTest.php
Normal file
86
tests/40_NamensabgleichTest.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fhp\Model\VopVerificationResult;
|
||||
use Finanzmanager\Services\FinTsUeberweisung;
|
||||
|
||||
/**
|
||||
* Namensabgleich vor der Überweisung (Verification of Payee).
|
||||
*
|
||||
* Seit Oktober 2025 gleicht die Bank den Empfängernamen mit dem Kontoinhaber
|
||||
* ab. Stimmt beides überein, bestätigt der Finanzmanager selbst und geht direkt
|
||||
* zur TAN; weicht der Name ab, wird gefragt. Geprüft wird hier, was sich ohne
|
||||
* Bankverbindung prüfen lässt: die Aufbereitung dessen, was die Bank meldet,
|
||||
* und dass ohne wartenden Vorgang niemand etwas bestätigen kann.
|
||||
*/
|
||||
class NamensabgleichTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 40: Namensabgleich vor der Überweisung';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->jedesErgebnisHatEinenDeutschenSatz();
|
||||
$this->derBanktextWirdKlartext();
|
||||
$this->ohneWartendenVorgangGehtNichts();
|
||||
}
|
||||
|
||||
private function jedesErgebnisHatEinenDeutschenSatz(): void
|
||||
{
|
||||
$ergebnisse = [
|
||||
VopVerificationResult::CompletedFullMatch,
|
||||
VopVerificationResult::CompletedCloseMatch,
|
||||
VopVerificationResult::CompletedNoMatch,
|
||||
VopVerificationResult::CompletedPartialMatch,
|
||||
VopVerificationResult::NotApplicable,
|
||||
];
|
||||
|
||||
$saetze = [];
|
||||
foreach ($ergebnisse as $ergebnis) {
|
||||
$satz = FinTsUeberweisung::vopUrteil($ergebnis);
|
||||
$this->assertNotEmpty($satz, 'Ergebnis ' . $ergebnis . ' hat einen Satz');
|
||||
$this->assert(
|
||||
!str_contains($satz, 'Completed') && !str_contains($satz, 'Match'),
|
||||
'Ergebnis ' . $ergebnis . ' steht auf Deutsch da'
|
||||
);
|
||||
$saetze[] = $satz;
|
||||
}
|
||||
|
||||
$this->assertEquals(count($ergebnisse), count(array_unique($saetze)), 'Jedes Ergebnis hat seinen eigenen Satz');
|
||||
|
||||
// Auch ein unbekanntes oder fehlendes Ergebnis darf den Dialog nicht leer lassen.
|
||||
$this->assertNotEmpty(FinTsUeberweisung::vopUrteil(null), 'Ohne Ergebnis steht trotzdem ein Satz da');
|
||||
$this->assertNotEmpty(FinTsUeberweisung::vopUrteil('Unbekannt'), 'Unbekanntes Ergebnis ebenso');
|
||||
}
|
||||
|
||||
private function derBanktextWirdKlartext(): void
|
||||
{
|
||||
// So liefern die Banken den Aufklärungstext: HTML mit Auszeichnung.
|
||||
$html = 'Der Name stimmt <b>nicht</b> überein.<br>Bitte prüfen.'
|
||||
. '<p><b>Wichtiger Hinweis:</b> Die Überweisung wird ohne Korrektur ausgeführt.</p>';
|
||||
|
||||
$text = FinTsUeberweisung::vopHinweis($html);
|
||||
|
||||
$this->assert(!str_contains($text, '<'), 'Die Auszeichnung ist weg');
|
||||
$this->assert(str_contains($text, 'Der Name stimmt nicht überein.'), 'Der Satz bleibt vollständig');
|
||||
$this->assert(str_contains($text, 'Bitte prüfen.'), 'Entities werden aufgelöst');
|
||||
$this->assert(str_contains($text, "überein.\nBitte prüfen."), 'Aus <br> wird ein Zeilenumbruch');
|
||||
$this->assert(
|
||||
str_contains($text, 'Wichtiger Hinweis: Die Überweisung wird ohne Korrektur ausgeführt.'),
|
||||
'Auch der letzte Absatz steht da'
|
||||
);
|
||||
$this->assert(!str_contains($text, "\n\n\n"), 'Keine Leerzeilenwüsten');
|
||||
|
||||
$this->assertEquals('', FinTsUeberweisung::vopHinweis(''), 'Ohne Text bleibt es leer');
|
||||
$this->assertEquals('', FinTsUeberweisung::vopHinweis(' '), 'Leerraum zählt als kein Text');
|
||||
}
|
||||
|
||||
private function ohneWartendenVorgangGehtNichts(): void
|
||||
{
|
||||
$this->assertNull(FinTsUeberweisung::offeneVopAnfrage(), 'Ohne Vorgang wartet kein Namensabgleich');
|
||||
|
||||
$ergebnis = FinTsUeberweisung::vopBestaetigen();
|
||||
$this->assertEquals('fehler', $ergebnis['status'], 'Ohne Vorgang lässt sich nichts bestätigen');
|
||||
$this->assertNotEmpty($ergebnis['meldung'], 'Und es gibt eine Erklärung dazu');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue