Finanzmanager/src/Services/FinTsUeberweisung.php
fds2610-bot 3caf1dcdca Fix #67: Übermittlungsprotokoll für Überweisungen und Bankkommunikation speichern
Der gesamte FinTS-Dialog (gesendete und empfangene Meldungen) wird je Durchlauf
in bank_protokolle gespeichert, im Erfolgs- wie im Fehlerfall. Im Zahlungsverkehr
öffnet der Status das Protokoll der Überweisung als modalen Dialog; unter
Online-Banking › Protokoll gibt es das Gesamtprotokoll mit Filter nach Bank,
Datum und Freitext.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 07:48:10 +02:00

692 lines
26 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\Services;
use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\SendSEPATransfer;
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;
use Finanzmanager\Auth;
use Finanzmanager\Models\Bankprotokoll;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Zahlungsauftrag;
/**
* Sendet einen Zahlungsauftrag über FinTS ab.
*
* Aufgebaut wie `FinTsAbruf`, nur kürzer: anmelden, Konto suchen, Überweisung
* einreichen. Eine TAN ist hier die Regel und nicht die Ausnahme – für
* Überweisungen verlangt sie jede Bank. Der Dialog wird deshalb genauso über
* die Sitzung fortgesetzt.
*
* **Erst die Bank, dann die Buchung.** Die Buchung auf dem Auftragskonto
* 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
{
public const SITZUNG = 'fints_ueberweisung';
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;
/** Schneidet den Dialog dieses Durchlaufs für das Übermittlungsprotokoll mit. */
private BankProtokollLogger $mitschnitt;
/**
* @param array<string,mixed> $zugang mit entschlüsselter PIN
* @param array<string,mixed> $zustand
*/
private function __construct(
private array $zugang,
private int $auftragId,
private array $zustand
) {
$this->mitschnitt = new BankProtokollLogger();
}
/**
* Startet das Absenden.
*
* @return array{status: string, meldung: string, tan?: array<string,mixed>, vop?: array<string,mixed>}
*/
public static function senden(int $auftragId): array
{
$auftrag = Zahlungsauftrag::find($auftragId);
if ($auftrag === null) {
return ['status' => 'fehler', 'meldung' => 'Den Zahlungsauftrag gibt es nicht.'];
}
if ($auftrag['status'] === 'gesendet') {
return ['status' => 'fehler', 'meldung' => 'Dieser Auftrag wurde bereits gesendet.'];
}
$zugangId = (int)($auftrag['zugang_id'] ?? 0);
if ($zugangId === 0) {
return [
'status' => 'fehler',
'meldung' => 'Dem Auftrag ist keine Bankverbindung zugeordnet.',
];
}
$zugang = Bankzugang::zugangsdaten($zugangId);
if ($zugang === null) {
return ['status' => 'fehler', 'meldung' => 'Die Bankverbindung gibt es nicht.'];
}
self::vergessen();
return (new self($zugang, $auftragId, ['schritt' => 'anmelden']))->lauf(null);
}
/**
* Setzt nach der TAN-Eingabe fort.
*
* @return array{status: string, meldung: string, tan?: array<string,mixed>, vop?: array<string,mixed>}
*/
public static function tanEinreichen(string $tan): array
{
$zustand = self::zustandLesen();
if ($zustand === null) {
return ['status' => 'fehler', 'meldung' => 'Es wartet keine Überweisung auf eine Freigabe.'];
}
$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(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;
}
/** @return array<string,mixed>|null */
public static function offeneTanAnfrage(): ?array
{
$zustand = self::zustandLesen();
return $zustand === null ? null : ($zustand['tan'] ?? null);
}
public static function abbrechen(): void
{
self::vergessen();
}
// ── Ablauf ───────────────────────────────────────────────────────────────
/**
* Ein Durchlauf samt Übermittlungsprotokoll: Was zwischen Finanzmanager und
* Bank gesendet und empfangen wurde, wird gespeichert – ob es klappt oder
* nicht.
*
* @return array<string,mixed>
*/
private function lauf(?string $tan, bool $vopBestaetigt = false): array
{
$auftrag = Zahlungsauftrag::find($this->auftragId);
if ($auftrag !== null) {
$this->mitschnitt->vermerke(sprintf(
'Überweisung %d: %s, IBAN %s, %s %s, Ausführung %s',
$this->auftragId,
$auftrag['empfaenger'],
$auftrag['iban'],
number_format((float)$auftrag['betrag'], 2, ',', '.'),
$auftrag['waehrung'],
$auftrag['ausfuehrung_am'] ?? 'sofort'
));
}
$ergebnis = $this->durchlauf($tan, $vopBestaetigt);
$status = match ($ergebnis['status']) {
'erfolg' => 'erfolg',
'tan', 'vop' => 'offen',
default => 'fehler',
};
$this->mitschnitt->vermerke('Ergebnis: ' . $ergebnis['meldung']);
try {
Bankprotokoll::speichern(
(int)$this->zugang['id'],
'ueberweisung',
$status,
(string)$ergebnis['meldung'],
$this->mitschnitt->text(),
$this->auftragId
);
} catch (\Throwable $e) {
// Ein Fehler im Protokoll darf die Überweisung nicht verdecken.
DebugLog::fehler($e);
}
return $ergebnis;
}
/** @return array<string,mixed> */
private function durchlauf(?string $tan, bool $vopBestaetigt): array
{
$auftrag = Zahlungsauftrag::find($this->auftragId);
if ($auftrag === null) {
self::vergessen();
return ['status' => 'fehler', 'meldung' => 'Den Zahlungsauftrag gibt es nicht mehr.'];
}
try {
$fints = $this->verbindung();
} catch (\Throwable $e) {
return $this->fehler('Verbindung fehlgeschlagen: ' . $e->getMessage());
}
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]);
if (!$aktion instanceof BaseAction) {
return $this->fehler('Der wartende Vorgang ist nicht mehr lesbar.');
}
if ($tan === '') {
return $this->tanNoetig($fints, $aktion, 'Überweisung', $this->zustand['schritt']);
}
$fints->submitTan($aktion, $tan);
if ($this->zustand['schritt'] === 'senden') {
return $this->nachDemSenden($fints, $aktion, true);
}
$this->zustand['schritt'] = $this->zustand['schritt'] === 'anmelden' ? 'konten' : 'senden';
unset($this->zustand['aktion'], $this->zustand['tan']);
}
if ($this->zustand['schritt'] === 'anmelden') {
$anmeldung = $fints->login();
if ($anmeldung->needsTan()) {
return $this->tanNoetig($fints, $anmeldung, 'Anmeldung', 'anmelden');
}
$this->zustand['schritt'] = 'konten';
}
if ($this->zustand['schritt'] === 'konten' || !isset($this->zustand['sepa'])) {
$konten = GetSEPAAccounts::create();
$fints->execute($konten);
if ($konten->needsTan()) {
return $this->tanNoetig($fints, $konten, 'Kontenabfrage', 'konten');
}
$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. '
. 'Bitte die IBAN im Konto prüfen.'
);
}
$this->zustand['sepa'] = serialize($sepa);
$this->zustand['schritt'] = 'senden';
}
if ($this->zustand['schritt'] === 'senden') {
$sepa = unserialize((string)$this->zustand['sepa'], ['allowed_classes' => [SEPAAccount::class]]);
if (!$sepa instanceof SEPAAccount) {
return $this->fehler('Das Auftragskonto ist nicht mehr lesbar. Bitte erneut senden.');
}
$formate = $this->sepaFormate($fints);
$schema = SepaXml::schemaWaehlen($formate);
if ($schema === null) {
return $this->fehler(
'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);
$fints->execute($aktion);
return $this->nachDemSenden($fints, $aktion);
}
} catch (\Throwable $e) {
DebugLog::fehler($e);
return $this->fehler($e->getMessage());
}
return $this->fehler('Der Auftrag konnte nicht abgeschlossen werden.');
}
/**
* Die SEPA-Datenformate, die die Bank annimmt.
*
* Sie stehen in den Bankparameterdaten (Segment HISPAS) und liegen nach der
* Anmeldung vor. Ältere Häuser führen ausschließlich das DK-Format
* pain.001.003.03 und weisen alles andere schon beim Einreichen ab.
*
* @return list<string>
*/
private function sepaFormate(FinTs $fints): array
{
try {
$hispas = $fints->getBpd()->getLatestSupportedParameters('HISPAS');
} catch (\Throwable) {
return [];
}
return $hispas instanceof HISPAS
? array_values($hispas->getParameter()->getUnterstuetzteSEPADatenformate())
: [];
}
/**
* 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>
*/
private function auftragsdaten(array $auftrag, SEPAAccount $sepa): array
{
$konto = Konto::find((int)$auftrag['konto_id']);
return [
'absender_name' => (string)(Einstellung::get('mandant_name') ?: ($konto['name'] ?? 'Auftraggeber')),
'absender_iban' => (string)($sepa->getIban() ?: ($konto['iban'] ?? '')),
'absender_bic' => (string)($sepa->getBic() ?? ''),
'empfaenger_name' => (string)$auftrag['empfaenger'],
'empfaenger_iban' => (string)$auftrag['iban'],
'empfaenger_bic' => (string)$auftrag['bic'],
'betrag' => (float)$auftrag['betrag'],
'waehrung' => (string)$auftrag['waehrung'],
'verwendungszweck' => (string)$auftrag['verwendungszweck'],
'ausfuehrung_am' => $auftrag['ausfuehrung_am'],
];
}
/**
* 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
*/
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);
$ibans[] = self::normiert((string)($konto['iban'] ?? ''));
$ibans = array_values(array_filter($ibans));
$kontonummern = array_values(array_filter($kontonummern));
foreach ($konten as $kandidat) {
if (in_array(self::normiert((string)$kandidat->getIban()), $ibans, true)) {
return $kandidat;
}
}
// 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> */
private function angenommen(FinTs $fints): array
{
Zahlungsauftrag::alsGesendetVermerken($this->auftragId, 'Von der Bank angenommen.');
self::vergessen();
try {
$fints->close();
} catch (\Throwable) {
// Der Dialog ist zu Ende; ein Fehler beim Abmelden ändert nichts.
}
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
{
$verfahren = $fints->getSelectedTanMode();
$anfrage = $aktion->getTanRequest();
$tan = [
'bezeichnung' => $bezeichnung,
'hinweis' => $anfrage?->getChallenge() ?? '',
'medium' => $anfrage?->getTanMediumName() ?? '',
'entkoppelt' => $verfahren !== null && $verfahren->isDecoupled(),
'bild_typ' => null,
'bild_daten' => null,
];
if (!$tan['entkoppelt'] && $anfrage?->getChallengeHhdUc()) {
try {
$bild = new TanRequestChallengeImage($anfrage->getChallengeHhdUc());
$tan['bild_typ'] = $bild->getMimeType();
$tan['bild_daten'] = base64_encode($bild->getData());
} catch (\Throwable) {
// Kein Bild – dann eben ohne.
}
}
self::zustandSchreiben([
'schritt' => $schritt,
'aktion' => serialize($aktion),
'dialog' => $fints->persist(),
'sepa' => $this->zustand['sepa'] ?? null,
'tan' => $tan,
'zugang_id' => (int)$this->zugang['id'],
'auftrag_id' => $this->auftragId,
]);
return [
'status' => 'tan',
'meldung' => 'Die Bank verlangt eine Freigabe.',
'tan' => $tan,
];
}
/** @return array{status: string, meldung: string} */
private function fehler(string $meldung): array
{
Zahlungsauftrag::alsFehlerVermerken($this->auftragId, $meldung);
self::vergessen();
return ['status' => 'fehler', 'meldung' => $meldung];
}
private function verbindung(): FinTs
{
$optionen = new FinTsOptions();
$optionen->url = (string)$this->zugang['fints_url'];
$optionen->bankCode = (string)$this->zugang['blz'];
$optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT;
$optionen->productVersion = self::PRODUKT_VERSION;
$fints = FinTs::new(
$optionen,
Credentials::create(
(string)$this->zugang['benutzerkennung'],
(string)$this->zugang['pin']
),
$this->zustand['dialog'] ?? null
);
$fints->setLogger($this->mitschnitt);
// Dieselbe Wahl wie beim Umsatzabruf – eine Überweisung soll nicht
// plötzlich ein anderes Verfahren verlangen als der Abruf.
if (!isset($this->zustand['dialog'])) {
FinTsVerfahren::anwenden(
$fints,
$this->zugang,
static fn(string $zeile) => DebugLog::schreibe('fints', $zeile)
);
}
return $fints;
}
/** @return array<string,mixed>|null */
private static function zustandLesen(): ?array
{
Auth::startSession();
$zustand = $_SESSION[self::SITZUNG] ?? null;
return is_array($zustand) ? $zustand : null;
}
/** @param array<string,mixed> $zustand */
private static function zustandSchreiben(array $zustand): void
{
Auth::startSession();
$_SESSION[self::SITZUNG] = $zustand;
}
private static function vergessen(): void
{
Auth::startSession();
unset($_SESSION[self::SITZUNG]);
}
}