string, 'format' => string|null] */ private $statements = []; /** * @param SEPAAccount $account Das Konto fuer das Auszuege abgerufen werden sollen. * @param string|null $format Gewuenschtes Format (FORMAT_MT940/FORMAT_ISO8583/FORMAT_PDF), null=Bank-Standard. * @param string|null $statementNumber Optionale Auszugsnummer. * @param string|null $year Optionales Jahr (JJJJ). * @return GetElectronicStatement */ public static function create( SEPAAccount $account, ?string $format = null, ?string $statementNumber = null, ?string $year = null ): GetElectronicStatement { $result = new GetElectronicStatement(); $result->account = $account; $result->format = $format; $result->statementNumber = $statementNumber; $result->year = $year; return $result; } /** * @deprecated */ public function serialize(): string { return serialize($this->__serialize()); } public function __serialize(): array { return [ parent::__serialize(), $this->account, $this->format, $this->statementNumber, $this->year, ]; } /** * @deprecated */ public function unserialize($serialized) { self::__unserialize(unserialize($serialized)); } public function __unserialize(array $serialized): void { list( $parentSerialized, $this->account, $this->format, $this->statementNumber, $this->year ) = $serialized; is_array($parentSerialized) ? parent::__unserialize($parentSerialized) : parent::unserialize($parentSerialized); } /** * @return array Array von ['data' => string, 'format' => string|null] pro Kontoauszug. */ public function getStatements(): array { $this->ensureDone(); return $this->statements; } /** * Hilfsfunktion: Gibt nur die PDF-Daten zurueck (filtert nach Format=3 oder erkennt %PDF-). * @return string[] Array von PDF-Binaerdaten. */ public function getPdfStatements(): array { $this->ensureDone(); $pdfs = []; foreach ($this->statements as $stmt) { $data = $stmt['data']; $format = $stmt['format'] ?? null; // PDF wenn Format=3 oder Daten mit %PDF- beginnen if ($format === self::FORMAT_PDF || str_starts_with($data, '%PDF-')) { $pdfs[] = $data; } } return $pdfs; } /** * Ermittelt die hoechste unterstuetzte HIEKAS-Version aus den BPD. * Funktioniert auch mit AnonymousSegments (wenn unsere typisierten Klassen * nicht zur Bank-Antwort passen). */ private function resolveHiekasVersion(BPD $bpd): int { // Erst typisierte Segmente versuchen $hiekas = $bpd->getLatestSupportedParameters('HIEKAS'); if ($hiekas !== null) { return $hiekas->getVersion(); } // Fallback: Version aus anonymen BPD-Segmenten lesen if (isset($bpd->parameters['HIEKAS'])) { $versions = array_keys($bpd->parameters['HIEKAS']); // Bereits absteigend sortiert (krsort in BPD::extractFromResponse) $version = reset($versions); error_log("[BankImport HKEKA] HIEKAS nur als AnonymousSegment verfuegbar, Version=" . $version); return (int) $version; } throw new UnsupportedException('HIEKAS nicht in BPD gefunden'); } /** {@inheritdoc} */ protected function createRequest(BPD $bpd, ?UPD $upd) { error_log("[BankImport HKEKA] createRequest() aufgerufen"); $version = $this->resolveHiekasVersion($bpd); error_log("[BankImport HKEKA] HIEKAS Version=" . $version); error_log("[BankImport HKEKA] Account IBAN=" . $this->account->getIban() . ", BIC=" . $this->account->getBic()); error_log("[BankImport HKEKA] Format=" . ($this->format ?: 'null (Bank-Standard)') . ", Auszugsnummer=" . ($this->statementNumber ?: 'null') . ", Jahr=" . ($this->year ?: 'null')); switch ($version) { case 3: error_log("[BankImport HKEKA] Erstelle HKEKAv3 (KtvV3-basiert, BLZ)"); return HKEKAv3::create( KtvV3::fromAccount($this->account), $this->format, $this->statementNumber, $this->year ); case 4: error_log("[BankImport HKEKA] Erstelle HKEKAv4 (Kti-basiert, IBAN/BIC)"); return HKEKAv4::create( Kti::fromAccount($this->account), $this->format, $this->statementNumber, $this->year ); case 5: error_log("[BankImport HKEKA] Erstelle HKEKAv5 (Kti-basiert, IBAN/BIC)"); return HKEKAv5::create( Kti::fromAccount($this->account), $this->format, $this->statementNumber, $this->year ); default: error_log("[BankImport HKEKA] FEHLER: Nicht unterstuetzte Version " . $version); throw new UnsupportedException('Nicht unterstuetzte HKEKA-Version: ' . $version); } } /** * Loggt Diagnose-Informationen ueber anonyme HIEKA-Segmente in der Antwort. * Wird aufgerufen wenn findSegments(HIEKA::class) leer ist, um die tatsaechliche * Segment-Struktur der Bank zu analysieren. */ private function logAnonymousHiekaSegments(Message $response): void { foreach ($response->plainSegments as $seg) { $name = $seg->getName(); if ($name !== 'HIEKA') { continue; } $version = $seg->getVersion(); $class = get_class($seg); error_log("[BankImport HKEKA] DIAGNOSE: Segment {$name}v{$version} class={$class}"); if ($seg instanceof AnonymousSegment) { // Reflection um private 'elements' zu lesen try { $ref = new \ReflectionClass($seg); $elProp = $ref->getProperty('elements'); $elProp->setAccessible(true); $elements = $elProp->getValue($seg); error_log("[BankImport HKEKA] DIAGNOSE: " . count($elements) . " Elemente im Segment"); foreach ($elements as $idx => $el) { if ($el === null) { error_log("[BankImport HKEKA] [{$idx}] NULL (leer)"); } elseif (is_array($el)) { // DEG (Data Element Group) $parts = array_map(function ($v) { if ($v === null) return 'NULL'; $s = (string) $v; return strlen($s) > 40 ? substr($s, 0, 40) . '...(' . strlen($s) . 'B)' : $s; }, $el); error_log("[BankImport HKEKA] [{$idx}] DEG: " . implode(' : ', $parts)); } else { $val = (string) $el; if (strlen($val) > 80) { // Binaerdaten oder lange Strings kuerzen $hex = bin2hex(substr($val, 0, 16)); error_log("[BankImport HKEKA] [{$idx}] BIN/LANG: " . strlen($val) . " Bytes, Hex-Start=" . $hex . ", Text-Start=" . substr($val, 0, 30)); } else { error_log("[BankImport HKEKA] [{$idx}] " . $val); } } } } catch (\Throwable $e) { error_log("[BankImport HKEKA] DIAGNOSE: Reflection fehlgeschlagen: " . $e->getMessage()); } } } } /** {@inheritdoc} */ public function processResponse(Message $response) { error_log("[BankImport HKEKA] processResponse() aufgerufen"); parent::processResponse($response); // Bank sendet 3010 wenn keine Auszuege verfuegbar $isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null; $responseSegments = $response->findSegments(HIEKA::class); error_log("[BankImport HKEKA] isUnavailable=" . ($isUnavailable ? 'JA' : 'NEIN') . ", HIEKA-Segmente=" . count($responseSegments) . ", Request-Segment-Nummern=" . implode(',', $this->getRequestSegmentNumbers())); // Alle Rueckmeldungen loggen try { $rueckmeldungen = $response->findSegments(\Fhp\Segment\HIRMS\HIRMSv2::class); foreach ($rueckmeldungen as $hirms) { foreach ($hirms->rueckmeldung as $rm) { error_log("[BankImport HKEKA] Rueckmeldung: Code=" . $rm->rueckmeldungscode . " Ref=" . $rm->bezugsdatenelement . " Text=" . $rm->rueckmeldungstext); } } } catch (\Throwable $e) { error_log("[BankImport HKEKA] Rueckmeldungen konnten nicht gelesen werden: " . $e->getMessage()); } // Wenn keine typisierten HIEKA-Segmente gefunden: Diagnose-Logging if (count($responseSegments) === 0) { error_log("[BankImport HKEKA] Keine typisierten HIEKA-Segmente, pruefe anonyme..."); $this->logAnonymousHiekaSegments($response); } if (!$isUnavailable && count($responseSegments) === 0 && count($this->getRequestSegmentNumbers()) > 0) { error_log("[BankImport HKEKA] FEHLER: Keine HIEKA-Segmente in Antwort!"); throw new UnexpectedResponseException('Keine HIEKA-Antwort-Segmente erhalten!'); } /** @var HIEKA $hieka */ foreach ($responseSegments as $segIdx => $hieka) { error_log("[BankImport HKEKA] Verarbeite HIEKA-Segment " . ($segIdx + 1) . "/" . count($responseSegments)); $format = $hieka->getKontoauszugsformat(); error_log("[BankImport HKEKA] Format=" . ($format ?: 'null') . " (1=MT940, 2=ISO8583, 3=PDF)"); $data = $hieka->getKontoauszug()->getData(); $rawLen = strlen($data); $rawStart = substr($data, 0, 20); error_log("[BankImport HKEKA] Rohdaten: " . $rawLen . " Bytes, Anfang='" . $rawStart . "', Hex=" . bin2hex(substr($data, 0, 10))); // Pruefen ob Base64-kodiert if ($format === self::FORMAT_PDF && !str_starts_with($data, '%PDF-')) { error_log("[BankImport HKEKA] PDF-Format aber beginnt NICHT mit %PDF-, pruefe Base64..."); $decoded = base64_decode($data, true); if ($decoded !== false && str_starts_with($decoded, '%PDF-')) { error_log("[BankImport HKEKA] Base64-Dekodierung erfolgreich! " . strlen($decoded) . " Bytes"); $data = $decoded; } else { error_log("[BankImport HKEKA] WARNUNG: Base64-Dekodierung fehlgeschlagen, verwende Rohdaten"); } } elseif ($format === null && !str_starts_with($data, '%PDF-')) { // Kein Format angegeben, trotzdem Base64 pruefen $decoded = base64_decode($data, true); if ($decoded !== false && str_starts_with($decoded, '%PDF-')) { error_log("[BankImport HKEKA] Ohne Format-Angabe: Base64-PDF erkannt! " . strlen($decoded) . " Bytes"); $data = $decoded; $format = self::FORMAT_PDF; } } // Quittung pruefen $quittung = $hieka->getQuittung(); if ($quittung !== null) { error_log("[BankImport HKEKA] Quittung vorhanden: " . strlen($quittung->getData()) . " Bytes"); } if (!empty($data)) { $this->statements[] = [ 'data' => $data, 'format' => $format, ]; error_log("[BankImport HKEKA] Statement hinzugefuegt (Format=" . ($format ?: 'unbekannt') . ", gesamt: " . count($this->statements) . ")"); } else { error_log("[BankImport HKEKA] WARNUNG: Leere Daten, uebersprungen"); } } error_log("[BankImport HKEKA] processResponse() fertig, " . count($this->statements) . " Statements gesammelt"); } }