$zugang mit entschlüsselter PIN * @param array $zustand */ private function __construct( private array $zugang, private int $auftragId, private array $zustand ) { } /** * Startet das Absenden. * * @return array{status: string, meldung: string, tan?: 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.']; } 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} */ 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)); } public static function wartetAufTan(): bool { return self::zustandLesen() !== null; } /** @return array|null */ public static function offeneTanAnfrage(): ?array { $zustand = self::zustandLesen(); return $zustand === null ? null : ($zustand['tan'] ?? null); } public static function abbrechen(): void { self::vergessen(); } // ── Ablauf ─────────────────────────────────────────────────────────────── /** @return array */ private function lauf(?string $tan): 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 { // 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->angenommen($fints); } $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 = $this->passendesKonto($konten->getAccounts(), (int)$auftrag['konto_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.'); } $xml = SepaXml::ueberweisung($this->auftragsdaten($auftrag, $sepa)); $aktion = SendSEPATransfer::create($sepa, $xml); $fints->execute($aktion); if ($aktion->needsTan()) { return $this->tanNoetig($fints, $aktion, 'Überweisung', 'senden'); } return $this->angenommen($fints); } } catch (\Throwable $e) { DebugLog::fehler($e); return $this->fehler($e->getMessage()); } return $this->fehler('Der Auftrag konnte nicht abgeschlossen werden.'); } /** * @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'], ]; } /** * @param list $konten */ private function passendesKonto(array $konten, int $kontoId): ?SEPAAccount { $konto = Konto::find($kontoId); $iban = strtoupper(str_replace(' ', '', (string)($konto['iban'] ?? ''))); foreach ($konten as $kandidat) { if ($iban !== '' && strtoupper((string)$kandidat->getIban()) === $iban) { return $kandidat; } } // Ohne IBAN am Konto: Hat die Bank nur ein Konto, ist es eindeutig. return $iban === '' && count($konten) === 1 ? $konten[0] : null; } /** @return array */ 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.']; } /** @return array */ 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(new DebugLogAdapter()); // 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]); } }