$zugang mit entschlüsselter PIN * @param array $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, vop?: array} */ 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.']; } // Wartet noch ein anderer Durchlauf auf seine Freigabe, verfällt er jetzt – // das soll im Protokoll stehen und nicht stillschweigend passieren. self::abbrechen('Abgelöst durch einen neuen Sendeversuch, bevor die Freigabe erteilt war.'); return (new self($zugang, $auftragId, ['schritt' => 'anmelden']))->lauf(null); } /** * Setzt nach der TAN-Eingabe fort. * * @return array{status: string, meldung: string, tan?: array, vop?: array} */ 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, vop?: array} */ 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|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; } /** Der Auftrag, dessen Freigabe in dieser Sitzung gerade aussteht. */ public static function wartenderAuftrag(): ?int { $auftragId = (int)(self::zustandLesen()['auftrag_id'] ?? 0); return $auftragId === 0 ? null : $auftragId; } /** @return array|null */ public static function offeneTanAnfrage(): ?array { $zustand = self::zustandLesen(); return $zustand === null ? null : ($zustand['tan'] ?? null); } /** * Verwirft eine ausstehende Freigabe. * * Die Bank hat den Auftrag dann zwar gesehen und eine TAN angefordert, führt * ihn ohne Freigabe aber nicht aus. Genau das muss sich später nachweisen * lassen – deshalb ein Eintrag im Übermittlungsprotokoll und eine Meldung am * Auftrag, statt den Zustand nur aus der Sitzung zu löschen. */ public static function abbrechen(string $grund = 'Vom Nutzer abgebrochen.'): void { $zustand = self::zustandLesen(); self::vergessen(); $auftragId = (int)($zustand['auftrag_id'] ?? 0); if ($auftragId === 0) { return; } $meldung = 'Freigabe nicht erteilt – keine TAN eingereicht, die Bank führt den Auftrag nicht aus. ' . $grund; try { $mitschnitt = new BankProtokollLogger(); $mitschnitt->vermerke(sprintf( 'Ausstehender Schritt: %s', ($zustand['schritt'] ?? '') === 'vop' ? 'Namensabgleich bestätigen' : 'TAN eingeben' )); $mitschnitt->vermerke('Ergebnis: ' . $meldung); Bankprotokoll::speichern( (int)($zustand['zugang_id'] ?? 0), 'ueberweisung', 'abgebrochen', $meldung, $mitschnitt->text(), $auftragId ); Zahlungsauftrag::alsFehlerVermerken($auftragId, $meldung); } catch (\Throwable $e) { DebugLog::fehler($e); } } // ── Ablauf ─────────────────────────────────────────────────────────────── /** * Ein Durchlauf samt Übermittlungsprotokoll: Was zwischen Finanzmanager und * Bank gesendet und empfangen wurde, wird gespeichert – ob es klappt oder * nicht. * * @return array */ private function lauf(?string $tan, bool $vopBestaetigt = false): array { $auftrag = Zahlungsauftrag::find($this->auftragId); if ($auftrag !== null) { $this->mitschnitt->vermerke(sprintf( '%s %d: %s, IBAN %s, %s %s, Ausführung %s', self::istEchtzeit($auftrag) ? 'Echtzeitüberweisung' : 'Überweisung', $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']); if ($status === 'offen') { Zahlungsauftrag::alsWartendVermerken($this->auftragId, (string)$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 */ 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.'); } $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( ($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 = $echtzeit // Keine Umwandlung in eine gewöhnliche Überweisung – siehe oben. ? FinTsEchtzeitUeberweisung::create($sepa, $xml, false) : SendSEPATransfer::create($sepa, $xml); try { $fints->execute($aktion); } catch (UnsupportedException $e) { if (!self::istBekannteVopTanLuecke($e)) { throw $e; } // Der Auftrag hat Namensabgleich und TAN-Anfrage trotz der // Ausnahme schon erhalten (siehe unten) – ganz normal weiter. } 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 */ private function sepaFormate(FinTs $fints): array { try { $hispas = $fints->getBpd()->getLatestSupportedParameters('HISPAS'); } catch (\Throwable) { return []; } return $hispas instanceof HISPAS ? array_values($hispas->getParameter()->getUnterstuetzteSEPADatenformate()) : []; } /** * 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. * * @param bool $vopErledigt Ob die VOP-Bestätigung schon gegeben wurde – sie * darf nicht zweimal automatisch erfolgen. * @return array */ 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, $aktion); } /** * Ob eine Ausnahme die bekannte Lücke von php-fints (Stand v4.1.4) ist: * Bestätigt die Bank den Namensabgleich mit einem Volltreffer und verlangt * im selben Antwortsatz schon die TAN, kennt die Bibliothek diese * Kombination nicht und wirft eine rohe `UnsupportedException` – obwohl sie * dem Vorgang (`BaseAction::setTanRequest()` und * `::setVopConfirmationRequest()`) beide Angaben schon mitgegeben hat, bevor * sie wirft. `nachDemSenden()` bestätigt einen Volltreffer ohnehin * automatisch (siehe oben) – die Ausnahme muss also nur abgefangen werden, * damit ihr englischer Text nicht als Fehlermeldung beim Nutzer landet. */ private static function istBekannteVopTanLuecke(UnsupportedException $e): bool { return $e->getMessage() === 'Combined VOP match confirmation and TAN request'; } /** * 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 $auftrag * @return array */ 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'], 'echtzeit' => self::istEchtzeit($auftrag), ]; } /** * 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 $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 */ private function angenommen(FinTs $fints, ?BaseAction $aktion = null): array { $auftrag = Zahlungsauftrag::find($this->auftragId); $echtzeit = $auftrag !== null && self::istEchtzeit($auftrag); // Die Bank hat die TAN angenommen, den Ausgang aber noch nicht gemeldet. // Der Auftrag gilt als gesendet – ein erneutes Senden wäre eine // Doppelzahlung –, der Nutzer prüft den Umsatz. $statusOffen = $aktion instanceof FinTsEchtzeitUeberweisung && $aktion->istStatusOffen(); Zahlungsauftrag::alsGesendetVermerken( $this->auftragId, $statusOffen ? 'Die Bank hat die Echtzeitüberweisung entgegengenommen, den Ausgang aber noch nicht bestätigt.' : ($echtzeit ? 'Von der Bank als Echtzeitüberweisung angenommen.' : 'Von der Bank angenommen.') ); self::vergessen(); try { $fints->close(); } catch (\Throwable) { // Der Dialog ist zu Ende; ein Fehler beim Abmelden ändert nichts. } if ($statusOffen) { return [ 'status' => 'erfolg', 'meldung' => 'Die Bank hat die Echtzeitüberweisung entgegengenommen, den Ausgang aber noch nicht bestätigt. ' . 'Bitte im Kontoumsatz prüfen, ob sie ausgeführt wurde, und nicht erneut senden.', ]; } return [ 'status' => 'erfolg', 'meldung' => $echtzeit ? 'Die Echtzeitüberweisung wurde an die Bank übergeben.' : 'Die Überweisung wurde an die Bank übergeben.', ]; } /** * Der Namensabgleich weicht ab – der Nutzer muss entscheiden. * * @return array */ 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. */ public static function vopHinweis(string $html): string { return BankFreitext::klartext($html); } /** * 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 */ private function tanNoetig(FinTs $fints, BaseAction $aktion, string $bezeichnung, string $schritt): array { $verfahren = $fints->getSelectedTanMode(); $anfrage = $aktion->getTanRequest(); $tan = [ 'bezeichnung' => $bezeichnung, 'hinweis' => StartcodeErkennung::hervorheben( $anfrage?->getChallenge() ?? '', (string)($this->zugang['startcode_muster'] ?? '') ), '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|null */ private static function zustandLesen(): ?array { Auth::startSession(); $zustand = $_SESSION[self::SITZUNG] ?? null; return is_array($zustand) ? $zustand : null; } /** @param array $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]); } }