From 822458ee0fc0c8827b621532d632e023ad4e8574 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Fri, 25 Sep 2026 15:21:04 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#71:=20Echtzeit=C3=BCberweisung=20im=20?= =?UTF-8?q?=C3=9Cberweisungsdialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Überweisungsdialog bietet das Häkchen „Echtzeitüberweisung“. SEPA behandelt sie als eigenes Verfahren: Über FinTS geht der Auftrag als HKIPZ (SendSEPARealtimeTransfer) statt als HKCCS hinaus, die pain.001-Nachricht trägt die Zahlungsart LclInstrm/Cd = INST (nur im ISO-Format pain.001.001.03). Eine Echtzeitüberweisung hat keinen Ausführungstermin; bietet die Bank sie nicht an, scheitert der Auftrag mit klarer Meldung, eine stille Umwandlung in eine gewöhnliche Überweisung ist abgeschaltet. Neue Spalte zahlungsauftraege.echtzeit (Migration 019), Test 43 und ein E2E-Test für das Sperren des Termins. Co-Authored-By: Claude Opus 5.5 --- Changes.md | 20 ++ README.md | 2 +- docs/nebenmodule.md | 13 ++ migrations/019_echtzeitueberweisung.sql | 11 + public/static/app.js | 16 ++ src/Controllers/ZahlungsverkehrController.php | 1 + src/Models/Zahlungsauftrag.php | 26 ++- src/Services/FinTsUeberweisung.php | 85 +++++++- src/Services/SepaXml.php | 36 +++- src/Views/zahlungsverkehr/formular.php | 11 + src/Views/zahlungsverkehr/index.php | 12 +- tests/43_EchtzeitueberweisungTest.php | 204 ++++++++++++++++++ tests/e2e/specs/module.spec.js | 15 ++ 13 files changed, 429 insertions(+), 23 deletions(-) create mode 100644 migrations/019_echtzeitueberweisung.sql create mode 100644 tests/43_EchtzeitueberweisungTest.php diff --git a/Changes.md b/Changes.md index 57683d9..b4bfa59 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,25 @@ # Änderungen +## Echtzeitüberweisung im Zahlungsverkehr (25.09.2026) + +Der Überweisungsdialog bietet jetzt das Häkchen **Echtzeitüberweisung**: Das Geld ist binnen +Sekunden beim Empfänger. SEPA behandelt die Echtzeitüberweisung als eigenes Verfahren +(SCT Inst), der Finanzmanager deshalb auch. + +- Über FinTS geht der Auftrag als Geschäftsvorfall HKIPZ hinaus (`SendSEPARealtimeTransfer` + aus php-fints) statt als HKCCS. In der pain.001-Nachricht steht die Zahlungsart + `LclInstrm/Cd = INST`; das gibt es nur im ISO-Format `pain.001.001.03`. +- Eine Echtzeitüberweisung hat keinen Ausführungstermin: Der Dialog sperrt das Feld, ein + Termin in der Zukunft wird beim Speichern abgewiesen. +- Bietet die Bank keine Echtzeitüberweisung an, scheitert der Auftrag mit einer klaren + Meldung und bleibt stehen. Eine stille Umwandlung in eine gewöhnliche Überweisung durch + die Bank ist abgeschaltet. +- Die Übersicht kennzeichnet Echtzeitüberweisungen in der Spalte „Ausführung“, das + Übermittlungsprotokoll nennt sie ausdrücklich. +- Neue Spalte `zahlungsauftraege.echtzeit` (Migration `019`). +- Test 43 (`tests/43_EchtzeitueberweisungTest.php`) und ein E2E-Test für das Sperren des + Termins. + ## Übermittlungsprotokoll für Überweisungen und Bankkommunikation (21.09.2026) Als Nachweis gegenüber der Bank, dass eine Überweisung fristgerecht und korrekt übermittelt diff --git a/README.md b/README.md index e4bcab5..8450fb3 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Haushalt wird beides parallel genutzt. eines wählbaren Zeitraums. * **Nebenmodule** – Buchungsvorlagen, regelmäßige Buchungen mit Fälligkeitslauf und Finanzkalender, Erinnerungen, Adressen, Verträge, Sparziele, Kredite mit Tilgungsplan. -* **Zahlungsverkehr** – Überweisungen und Terminüberweisungen über FinTS. +* **Zahlungsverkehr** – Überweisungen, Terminüberweisungen und Echtzeitüberweisungen über FinTS. * **Abgleich mit dem Bargeld-Tool** – wer Bargeldausgaben in einem zweiten Programm erfasst, sieht beim Zuweisen einer Kategorie sofort, ob die Buchung dort schon steht: grüner Haken, rotes X oder Ausrufezeichen in einer schmalen Spalte des Kontoblatts. diff --git a/docs/nebenmodule.md b/docs/nebenmodule.md index 92009e0..d402aef 100644 --- a/docs/nebenmodule.md +++ b/docs/nebenmodule.md @@ -131,6 +131,17 @@ 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. +**Echtzeitüberweisung.** Das Häkchen „Echtzeitüberweisung“ im Überweisungsdialog +(Spalte `zahlungsauftraege.echtzeit`) macht aus dem Auftrag eine SEPA-Echtzeitüberweisung +(SCT Inst): Das Geld ist binnen Sekunden beim Empfänger. SEPA behandelt sie als eigenes +Verfahren – über FinTS geht sie als Geschäftsvorfall **HKIPZ** hinaus statt als HKCCS, in +der pain.001-Nachricht steht zusätzlich die Zahlungsart `PmtTpInf/LclInstrm/Cd = INST`. +Einen Ausführungstermin gibt es dafür nicht; der Dialog sperrt das Feld, das Modell weist +einen Termin in der Zukunft ab. Bietet die Bank keine Echtzeitüberweisung an (kein +Segment HIIPZS in den Bankparametern), scheitert der Auftrag mit einer klaren Meldung. +Eine Umwandlung in eine gewöhnliche Überweisung erlaubt der Finanzmanager der Bank +ausdrücklich nicht – wer Echtzeit wählt, soll erfahren, wenn es nicht klappt. + **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 @@ -147,6 +158,8 @@ anderen nur das ältere DK-Format `pain.001.003.03` – und weisen alles andere 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. +Die Echtzeitüberweisung gibt es nur im ISO-Format: Das DK-Format kennt die Zahlungsart +`LclInstrm` nicht. Ihre Formate nennt die Bank in HIIPZS, ersatzweise gelten die aus HISPAS. 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 diff --git a/migrations/019_echtzeitueberweisung.sql b/migrations/019_echtzeitueberweisung.sql new file mode 100644 index 0000000..d252d7f --- /dev/null +++ b/migrations/019_echtzeitueberweisung.sql @@ -0,0 +1,11 @@ +-- Echtzeitüberweisung (SEPA Instant Credit Transfer). +-- +-- Eine Echtzeitüberweisung ist im SEPA-Verfahren ein eigener Geschäftsvorfall: +-- Über FinTS geht sie als HKIPZ statt als HKCCS hinaus, und in der +-- pain.001-Nachricht steht die Zahlungsart INST. Sie wird sofort ausgeführt, +-- einen Ausführungstermin gibt es dafür nicht. +-- +-- Idempotent über IF NOT EXISTS. + +ALTER TABLE zahlungsauftraege + ADD COLUMN IF NOT EXISTS echtzeit TINYINT(1) NOT NULL DEFAULT 0 AFTER ausfuehrung_am; diff --git a/public/static/app.js b/public/static/app.js index 1fa92ea..7df8d62 100644 --- a/public/static/app.js +++ b/public/static/app.js @@ -192,6 +192,22 @@ }); } + // Echtzeitüberweisung: Sie geht sofort hinaus, ein Ausführungstermin hat + // daneben keinen Sinn. Das Feld wird gesperrt, solange sie gewählt ist – + // die Prüfung beim Speichern bleibt trotzdem die maßgebliche. + const echtzeit = document.querySelector('[data-echtzeit]'); + const termin = document.getElementById('ausfuehrung_am'); + if (echtzeit && termin) { + const abgleichen = function () { + termin.disabled = echtzeit.checked; + if (echtzeit.checked) { + termin.value = ''; + } + }; + echtzeit.addEventListener('change', abgleichen); + abgleichen(); + } + // Kontenzuordnung einer Bankverbindung: Die Felder für ein neues Konto // stehen in einer eigenen Tabellenzeile und werden erst sichtbar, wenn // „neues Konto anlegen“ gewählt ist. Ausgeblendet wird hier und nicht im diff --git a/src/Controllers/ZahlungsverkehrController.php b/src/Controllers/ZahlungsverkehrController.php index 98c18d4..4aac4f2 100644 --- a/src/Controllers/ZahlungsverkehrController.php +++ b/src/Controllers/ZahlungsverkehrController.php @@ -303,6 +303,7 @@ class ZahlungsverkehrController extends BaseController 'waehrung' => $this->post('waehrung', 'EUR'), 'verwendungszweck' => $this->post('verwendungszweck'), 'ausfuehrung_am' => $this->post('ausfuehrung_am'), + 'echtzeit' => isset($_POST['echtzeit']), 'kategorie_id' => $this->post('kategorie_id'), 'klasse_id' => $this->post('klasse_id'), ]; diff --git a/src/Models/Zahlungsauftrag.php b/src/Models/Zahlungsauftrag.php index 8a5253f..5726dd6 100644 --- a/src/Models/Zahlungsauftrag.php +++ b/src/Models/Zahlungsauftrag.php @@ -11,7 +11,9 @@ use Finanzmanager\Mandant; * Überweisungen: erfasst, geprüft, über FinTS abgesendet. * * Der Auftrag bleibt als Beleg stehen, auch wenn die Bank ihn ablehnt – mit der - * Meldung daneben. Erst wenn die Bank ihn angenommen hat, entsteht die + * Meldung daneben. Eine Überweisung geht als gewöhnliche Überweisung, + * Terminüberweisung oder Echtzeitüberweisung hinaus; die Echtzeitüberweisung + * behandelt SEPA als eigenen Geschäftsvorfall und kennt keinen Termin. Erst wenn die Bank ihn angenommen hat, entsteht die * zugehörige Buchung auf dem Auftragskonto; vorher wäre sie eine Behauptung. */ class Zahlungsauftrag @@ -63,12 +65,12 @@ class Zahlungsauftrag Database::execute( 'INSERT INTO zahlungsauftraege (benutzer_id, konto_id, zugang_id, empfaenger, iban, bic, betrag, waehrung, - verwendungszweck, ausfuehrung_am, kategorie_id, klasse_id, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + verwendungszweck, ausfuehrung_am, echtzeit, kategorie_id, klasse_id, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ Mandant::id(), $f['konto_id'], $f['zugang_id'], $f['empfaenger'], $f['iban'], $f['bic'], $f['betrag'], $f['waehrung'], $f['verwendungszweck'], - $f['ausfuehrung_am'], $f['kategorie_id'], $f['klasse_id'], 'entwurf', + $f['ausfuehrung_am'], $f['echtzeit'], $f['kategorie_id'], $f['klasse_id'], 'entwurf', ] ); @@ -91,12 +93,12 @@ class Zahlungsauftrag Database::execute( 'UPDATE zahlungsauftraege SET konto_id = ?, zugang_id = ?, empfaenger = ?, iban = ?, bic = ?, betrag = ?, waehrung = ?, verwendungszweck = ?, ausfuehrung_am = ?, - kategorie_id = ?, klasse_id = ?, status = \'entwurf\', meldung = NULL + echtzeit = ?, kategorie_id = ?, klasse_id = ?, status = \'entwurf\', meldung = NULL WHERE id = ? AND benutzer_id = ?', [ $f['konto_id'], $f['zugang_id'], $f['empfaenger'], $f['iban'], $f['bic'], $f['betrag'], $f['waehrung'], $f['verwendungszweck'], $f['ausfuehrung_am'], - $f['kategorie_id'], $f['klasse_id'], $id, Mandant::id(), + $f['echtzeit'], $f['kategorie_id'], $f['klasse_id'], $id, Mandant::id(), ] ); } @@ -209,6 +211,15 @@ class Zahlungsauftrag throw new \InvalidArgumentException('Der Ausführungstermin liegt in der Vergangenheit.'); } + // Eine Echtzeitüberweisung ist binnen Sekunden beim Empfänger – einen + // Termin kennt sie nicht. Der heutige Tag ist ohnehin „sofort“. + $echtzeit = !empty($daten['echtzeit']); + if ($echtzeit && $termin !== '' && $termin > date('Y-m-d')) { + throw new \InvalidArgumentException( + 'Eine Echtzeitüberweisung wird sofort ausgeführt und kann keinen Ausführungstermin haben.' + ); + } + $id = static fn(mixed $wert): ?int => ($wert === null || $wert === '' || (int)$wert === 0) ? null : (int)$wert; @@ -223,7 +234,8 @@ class Zahlungsauftrag // 140 Zeichen sind das SEPA-Maß für den Verwendungszweck; was // darüber steht, würde die Bank ohnehin abschneiden. 'verwendungszweck' => mb_substr(trim((string)($daten['verwendungszweck'] ?? '')), 0, 140), - 'ausfuehrung_am' => $termin !== '' ? $termin : null, + 'ausfuehrung_am' => $termin !== '' && !$echtzeit ? $termin : null, + 'echtzeit' => $echtzeit ? 1 : 0, 'kategorie_id' => $id($daten['kategorie_id'] ?? null), 'klasse_id' => $id($daten['klasse_id'] ?? null), ]; diff --git a/src/Services/FinTsUeberweisung.php b/src/Services/FinTsUeberweisung.php index 091cb3a..416a728 100644 --- a/src/Services/FinTsUeberweisung.php +++ b/src/Services/FinTsUeberweisung.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Finanzmanager\Services; use Fhp\Action\GetSEPAAccounts; +use Fhp\Action\SendSEPARealtimeTransfer; use Fhp\Action\SendSEPATransfer; use Fhp\BaseAction; use Fhp\FinTs; @@ -14,6 +15,8 @@ use Fhp\Model\VopConfirmationRequest; use Fhp\Model\VopVerificationResult; use Fhp\Options\Credentials; use Fhp\Options\FinTsOptions; +use Fhp\Segment\IPZ\HIIPZSv1; +use Fhp\Segment\IPZ\HIIPZSv2; use Fhp\Segment\SPA\HISPAS; use Finanzmanager\Auth; use Finanzmanager\Models\Bankprotokoll; @@ -40,6 +43,13 @@ use Finanzmanager\Models\Zahlungsauftrag; * ü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. + * + * **Echtzeitüberweisung.** Ist sie am Auftrag gewählt, geht der Auftrag als + * eigener Geschäftsvorfall HKIPZ hinaus statt als HKCCS – so will es das + * SEPA-Verfahren. Bietet die Bank das nicht an, scheitert der Auftrag mit einer + * klaren Meldung und bleibt stehen. Eine stille Umwandlung in eine gewöhnliche + * Überweisung erlauben wir der Bank nicht: Wer Echtzeit wählt, braucht das Geld + * sofort beim Empfänger und soll erfahren, wenn das nicht klappt. */ class FinTsUeberweisung { @@ -185,7 +195,8 @@ class FinTsUeberweisung $auftrag = Zahlungsauftrag::find($this->auftragId); if ($auftrag !== null) { $this->mitschnitt->vermerke(sprintf( - 'Überweisung %d: %s, IBAN %s, %s %s, Ausführung %s', + '%s %d: %s, IBAN %s, %s %s, Ausführung %s', + self::istEchtzeit($auftrag) ? 'Echtzeitüberweisung' : 'Überweisung', $this->auftragId, $auftrag['empfaenger'], $auftrag['iban'], @@ -307,17 +318,29 @@ class FinTsUeberweisung return $this->fehler('Das Auftragskonto ist nicht mehr lesbar. Bitte erneut senden.'); } - $formate = $this->sepaFormate($fints); - $schema = SepaXml::schemaWaehlen($formate); + $echtzeit = self::istEchtzeit($auftrag); + if ($echtzeit && $this->echtzeitParameter($fints) === null) { + return $this->fehler( + 'Die Bank bietet über FinTS keine Echtzeitüberweisung an. ' + . 'Bitte die Echtzeitüberweisung am Auftrag abwählen und erneut senden.' + ); + } + + $formate = $echtzeit ? $this->echtzeitFormate($fints) : $this->sepaFormate($fints); + $schema = SepaXml::schemaWaehlen($formate, $echtzeit); if ($schema === null) { return $this->fehler( - 'Die Bank nimmt nur SEPA-Formate an, die der Finanzmanager nicht erzeugt: ' + ($echtzeit ? 'Für Echtzeitüberweisungen nimmt die Bank' : 'Die Bank nimmt') + . ' nur SEPA-Formate an, die der Finanzmanager nicht erzeugt: ' . implode(', ', $formate) ); } $xml = SepaXml::ueberweisung($this->auftragsdaten($auftrag, $sepa), $schema); - $aktion = SendSEPATransfer::create($sepa, $xml); + $aktion = $echtzeit + // Keine Umwandlung in eine gewöhnliche Überweisung – siehe oben. + ? SendSEPARealtimeTransfer::create($sepa, $xml, false) + : SendSEPATransfer::create($sepa, $xml); $fints->execute($aktion); return $this->nachDemSenden($fints, $aktion); @@ -352,6 +375,42 @@ class FinTsUeberweisung : []; } + /** + * Die Bankparameter der Echtzeitüberweisung (Segment HIIPZS) – oder `null`, + * wenn die Bank sie über FinTS nicht anbietet. + */ + private function echtzeitParameter(FinTs $fints): HIIPZSv1|HIIPZSv2|null + { + try { + $hiipzs = $fints->getBpd()->getLatestSupportedParameters('HIIPZS'); + } catch (\Throwable) { + return null; + } + + return $hiipzs instanceof HIIPZSv1 || $hiipzs instanceof HIIPZSv2 ? $hiipzs : null; + } + + /** + * Die SEPA-Datenformate für Echtzeitüberweisungen. Nennt die Bank dafür + * keine eigenen, gelten die allgemeinen aus HISPAS. + * + * @return list + */ + private function echtzeitFormate(FinTs $fints): array + { + $formate = $this->echtzeitParameter($fints)?->parameter->getUnterstuetzteSEPADatenformate(); + + return $formate === null || $formate === [] + ? $this->sepaFormate($fints) + : array_values($formate); + } + + /** @param array $auftrag */ + private static function istEchtzeit(array $auftrag): bool + { + return (int)($auftrag['echtzeit'] ?? 0) === 1; + } + /** * Was nach dem Einreichen des Auftrags noch zu tun ist: auf den * Namensabgleich warten, ihn bestätigen lassen, die TAN einholen. @@ -428,6 +487,7 @@ class FinTsUeberweisung 'waehrung' => (string)$auftrag['waehrung'], 'verwendungszweck' => (string)$auftrag['verwendungszweck'], 'ausfuehrung_am' => $auftrag['ausfuehrung_am'], + 'echtzeit' => self::istEchtzeit($auftrag), ]; } @@ -488,7 +548,13 @@ class FinTsUeberweisung /** @return array */ private function angenommen(FinTs $fints): array { - Zahlungsauftrag::alsGesendetVermerken($this->auftragId, 'Von der Bank angenommen.'); + $auftrag = Zahlungsauftrag::find($this->auftragId); + $echtzeit = $auftrag !== null && self::istEchtzeit($auftrag); + + Zahlungsauftrag::alsGesendetVermerken( + $this->auftragId, + $echtzeit ? 'Von der Bank als Echtzeitüberweisung angenommen.' : 'Von der Bank angenommen.' + ); self::vergessen(); try { @@ -497,7 +563,12 @@ class FinTsUeberweisung // Der Dialog ist zu Ende; ein Fehler beim Abmelden ändert nichts. } - return ['status' => 'erfolg', 'meldung' => 'Die Überweisung wurde an die Bank übergeben.']; + return [ + 'status' => 'erfolg', + 'meldung' => $echtzeit + ? 'Die Echtzeitüberweisung wurde an die Bank übergeben.' + : 'Die Überweisung wurde an die Bank übergeben.', + ]; } /** diff --git a/src/Services/SepaXml.php b/src/Services/SepaXml.php index 1b3dae8..deb5906 100644 --- a/src/Services/SepaXml.php +++ b/src/Services/SepaXml.php @@ -18,6 +18,12 @@ namespace Finanzmanager\Services; * Fassungen im Aufbau gleich (Reihenfolge und Pflichtfelder stimmen überein), * sie unterscheiden sich nur im Namensraum. Welches Format die Bank annimmt, * steht im FinTS-Segment HISPAS – siehe `Services\FinTsUeberweisung`. + * + * **Echtzeitüberweisung.** SEPA behandelt sie als eigenes Verfahren (SCT Inst): + * In der Nachricht steht zusätzlich die Zahlungsart `LclInstrm/Cd = INST`, und + * die Bank bekommt sie über einen eigenen Geschäftsvorfall (HKIPZ). Das ältere + * DK-Format pain.001.003.03 kennt die Zahlungsart nicht – eine + * Echtzeitüberweisung gibt es deshalb nur im ISO-Format. */ class SepaXml { @@ -27,6 +33,12 @@ 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]; + /** Die Formate, in denen sich eine Echtzeitüberweisung ausdrücken lässt. */ + private const SCHEMATA_ECHTZEIT = [self::SCHEMA_PAIN_001_001_03]; + + /** Zahlungsart (Local Instrument) der Echtzeitüberweisung. */ + public const ECHTZEIT = 'INST'; + /** * Das Ausführungsdatum für „sofort“. * @@ -44,7 +56,7 @@ class SepaXml * absender_name: string, absender_iban: string, absender_bic?: string, * empfaenger_name: string, empfaenger_iban: string, empfaenger_bic?: string, * betrag: float, waehrung?: string, verwendungszweck?: string, - * ausfuehrung_am?: ?string + * ausfuehrung_am?: ?string, echtzeit?: bool * } $auftrag * @param string $schema Namensraum der Nachricht, siehe `schemaWaehlen()`. */ @@ -55,12 +67,20 @@ class SepaXml throw new \InvalidArgumentException('Der Betrag muss größer als null sein.'); } + $echtzeit = !empty($auftrag['echtzeit']); + if ($echtzeit && !in_array($schema, self::SCHEMATA_ECHTZEIT, true)) { + throw new \InvalidArgumentException('Eine Echtzeitüberweisung gibt es nur im Format pain.001.001.03.'); + } + // Eindeutige Kennungen: Die Bank lehnt eine Nachricht ab, deren Kennung // sie schon kennt. $kennung = 'FM' . date('YmdHis') . substr(bin2hex(random_bytes(4)), 0, 6); $zeitpunkt = date('Y-m-d\TH:i:s'); - $ausfuehrung = self::ausfuehrungstag((string)($auftrag['ausfuehrung_am'] ?? '')); + // Eine Echtzeitüberweisung geht immer sofort hinaus. + $ausfuehrung = $echtzeit + ? self::SOFORT + : self::ausfuehrungstag((string)($auftrag['ausfuehrung_am'] ?? '')); $waehrung = strtoupper((string)($auftrag['waehrung'] ?? 'EUR')); $zweck = self::text((string)($auftrag['verwendungszweck'] ?? ''), 140); @@ -100,6 +120,11 @@ class SepaXml $ebene = $dokument->createElement('SvcLvl'); $art->appendChild($ebene); self::kind($dokument, $ebene, 'Cd', 'SEPA'); + if ($echtzeit) { + $zahlungsart = $dokument->createElement('LclInstrm'); + $art->appendChild($zahlungsart); + self::kind($dokument, $zahlungsart, 'Cd', self::ECHTZEIT); + } self::kind($dokument, $sammler, 'ReqdExctnDt', $ausfuehrung); @@ -193,15 +218,18 @@ class SepaXml * Vorzugsformat; meldet sie nur Formate, die hier nicht erzeugt werden, * kommt `null` zurück. * + * Für eine Echtzeitüberweisung kommt nur das ISO-Format infrage; führt die + * Bank ausschließlich das DK-Format, ist das Ergebnis ebenfalls `null`. + * * @param list $unterstuetzt */ - public static function schemaWaehlen(array $unterstuetzt): ?string + public static function schemaWaehlen(array $unterstuetzt, bool $echtzeit = false): ?string { if ($unterstuetzt === []) { return self::SCHEMA_PAIN_001_001_03; } - foreach (self::SCHEMATA as $schema) { + foreach ($echtzeit ? self::SCHEMATA_ECHTZEIT : self::SCHEMATA as $schema) { foreach ($unterstuetzt as $format) { if (str_starts_with(trim($format), $schema)) { return $schema; diff --git a/src/Views/zahlungsverkehr/formular.php b/src/Views/zahlungsverkehr/formular.php index d90950a..69291c6 100644 --- a/src/Views/zahlungsverkehr/formular.php +++ b/src/Views/zahlungsverkehr/formular.php @@ -119,6 +119,17 @@ include __DIR__ . '/../layout/kopf.php'; + +

+ Das Geld ist binnen Sekunden beim Empfänger. Eine Echtzeitüberweisung wird sofort + ausgeführt und hat keinen Ausführungstermin. Bietet die Bank sie nicht an, wird der + Auftrag abgewiesen und bleibt als Entwurf stehen. +

+