Finanzmanager/src/Controllers/ZahlungsverkehrController.php
fds2610-bot 822458ee0f Fix #71: Echtzeitüberweisung im Überweisungsdialog
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 <noreply@anthropic.com>
2026-09-25 15:21:04 +02:00

311 lines
9.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Finanzmanager\Controllers;
use Finanzmanager\Models\Adresse;
use Finanzmanager\Models\Bankprotokoll;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Zahlungsauftrag;
use Finanzmanager\Services\FinTsUeberweisung;
use Finanzmanager\View;
/**
* Zahlungsverkehr: Überweisungen erfassen, prüfen und über FinTS absenden.
*/
class ZahlungsverkehrController extends BaseController
{
public function index(): void
{
$this->verlangeAnmeldung();
$status = $this->query('status');
$this->render('zahlungsverkehr/index.php', [
'titel' => 'Zahlungsverkehr',
'auftraege' => Zahlungsauftrag::alle($status),
'status' => $status,
'offen' => Zahlungsauftrag::anzahlOffen(),
'wartetTan' => FinTsUeberweisung::wartetAufTan(),
]);
}
/**
* Das Übermittlungsprotokoll einer Überweisung. Das Skript holt es als
* Fragment in den Dialog; ohne Skript ist es eine eigene Seite.
*
* @param array<string,string> $params
*/
public function protokoll(array $params): void
{
$this->verlangeAnmeldung();
$auftrag = Zahlungsauftrag::find((int)$params['id']);
if ($auftrag === null) {
View::redirect('/zahlungsverkehr', 'Den Zahlungsauftrag gibt es nicht.', 'fehler');
return;
}
$this->render('zahlungsverkehr/protokoll.php', [
'titel' => 'Übermittlungsprotokoll',
'auftrag' => $auftrag,
'eintraege' => Bankprotokoll::fuerAuftrag((int)$auftrag['id']),
'fragment' => $this->istAjax(),
]);
}
public function neu(): void
{
$this->verlangeAnmeldung();
$this->render('zahlungsverkehr/formular.php', [
'titel' => 'Überweisung erfassen',
'auftrag' => null,
'konten' => $this->kontenMitBank(),
'adressen' => Adresse::alle(),
'kategorien' => Kategorie::baumFlach(null, true),
'klassen' => Klasse::alle(),
]);
}
public function anlegen(): void
{
$this->verlangeAnmeldung();
try {
$id = Zahlungsauftrag::anlegen($this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/zahlungsverkehr/neu', $e->getMessage(), 'fehler');
return;
}
// Auf Wunsch eine neue Adresse aus dem Empfänger anlegen – so wächst
// das Adressbuch beim Arbeiten, nicht durch Pflege.
if (isset($_POST['adresse_merken'])) {
try {
Adresse::anlegen([
'name' => $this->post('empfaenger'),
'iban' => $this->post('iban'),
'bic' => $this->post('bic'),
]);
} catch (\Throwable) {
// Gibt es die Adresse schon, ist nichts zu tun.
}
}
View::redirect('/zahlungsverkehr/' . $id, 'Überweisung erfasst. Sie ist noch nicht abgesendet.');
}
/** @param array<string,string> $params */
public function bearbeiten(array $params): void
{
$this->verlangeAnmeldung();
$auftrag = Zahlungsauftrag::find((int)$params['id']);
if ($auftrag === null) {
View::redirect('/zahlungsverkehr', 'Den Zahlungsauftrag gibt es nicht.', 'fehler');
return;
}
$konten = $this->kontenMitBank();
// Ein Auftrag, dessen Bank nachträglich entfernt wurde, soll sein
// Auftragskonto nicht verlieren – sonst würde beim Speichern
// unbemerkt ein anderes Konto gewählt.
if (!in_array((int)$auftrag['konto_id'], array_column($konten, 'id'), true)) {
$bisheriges = Konto::find((int)$auftrag['konto_id']);
if ($bisheriges !== null) {
$konten[] = $bisheriges;
}
}
$this->render('zahlungsverkehr/formular.php', [
'titel' => 'Überweisung an ' . $auftrag['empfaenger'],
'auftrag' => $auftrag,
'konten' => $konten,
'adressen' => Adresse::alle(),
'kategorien' => Kategorie::baumFlach(null, true),
'klassen' => Klasse::alle(),
]);
}
/** @param array<string,string> $params */
public function speichern(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
try {
Zahlungsauftrag::speichern($id, $this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/zahlungsverkehr/' . $id, $e->getMessage(), 'fehler');
return;
}
View::redirect('/zahlungsverkehr/' . $id, 'Überweisung gespeichert.');
}
/** @param array<string,string> $params */
public function loeschen(array $params): void
{
$this->verlangeAnmeldung();
try {
Zahlungsauftrag::loeschen((int)$params['id']);
} catch (\Throwable $e) {
View::redirect('/zahlungsverkehr', $e->getMessage(), 'fehler');
return;
}
View::redirect('/zahlungsverkehr', 'Überweisung gelöscht.');
}
/** @param array<string,string> $params */
public function stornieren(array $params): void
{
$this->verlangeAnmeldung();
Zahlungsauftrag::stornieren((int)$params['id']);
View::redirect('/zahlungsverkehr', 'Überweisung storniert.');
}
// ── Absenden ─────────────────────────────────────────────────────────────
/** @param array<string,string> $params */
public function senden(array $params): void
{
$this->verlangeAnmeldung();
$this->zeige(FinTsUeberweisung::senden((int)$params['id']));
}
public function tanFormular(): void
{
$this->verlangeAnmeldung();
$tan = FinTsUeberweisung::offeneTanAnfrage();
if ($tan === null) {
View::redirect('/zahlungsverkehr', 'Es wartet keine Überweisung auf eine Freigabe.');
return;
}
$this->render('zahlungsverkehr/tan.php', [
'titel' => 'Überweisung freigeben',
'tan' => $tan,
'meldung' => 'Die Bank verlangt eine Freigabe.',
]);
}
public function tanEinreichen(): void
{
$this->verlangeAnmeldung();
$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();
FinTsUeberweisung::abbrechen();
View::redirect('/zahlungsverkehr', 'Vorgang abgebrochen. Der Auftrag bleibt als Entwurf stehen.');
}
/** @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',
'tan' => $ergebnis['tan'] ?? [],
'meldung' => $ergebnis['meldung'],
]);
return;
}
View::redirect(
'/zahlungsverkehr',
$ergebnis['meldung'],
$ergebnis['status'] === 'erfolg' ? 'erfolg' : 'fehler'
);
}
/**
* Konten, denen eine Bankverbindung zugeordnet ist – nur die kommen als
* Auftragskonto infrage, denn ohne Bank lässt sich nichts senden.
*
* @return list<array<string,mixed>>
*/
private function kontenMitBank(): array
{
$zugangJeKonto = Bankzugang::zugangJeKonto();
return array_values(array_filter(
Konto::alle(true),
static fn(array $konto): bool => isset($zugangJeKonto[(int)$konto['id']])
));
}
/** @return array<string,mixed> */
private function formulardaten(): array
{
$kontoId = (int)$this->post('konto_id');
return [
'konto_id' => $kontoId,
// Die Bank ergibt sich aus dem Auftragskonto – eine eigene Auswahl
// gibt es im Formular nicht mehr.
'zugang_id' => Bankzugang::zugangJeKonto()[$kontoId] ?? null,
'empfaenger' => $this->post('empfaenger'),
'iban' => $this->post('iban'),
'bic' => $this->post('bic'),
'betrag' => str_replace(',', '.', $this->post('betrag')),
'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'),
];
}
}