← Code-Übersicht

HeadlessWetellSourceAdapter.php

Pfad: src/Infrastructure/Rechnung/HeadlessWetellSourceAdapter.php

Ext: php

Größe: 5742 Bytes

Geändert: 2026-07-16T14:13:27+02:00

Frühere Version vom 2026-07-16T14:13:27+02:00 · zur aktuellen Fassung

<?php
declare(strict_types=1);

namespace Demo\Infrastructure\Rechnung;

use Demo\Domain\Rechnung\SourceInvoice;
use Demo\Domain\Rechnung\WetellSourcePort;

/**
 * Source-Adapter ueber externen Headless-Service.
 */
final class HeadlessWetellSourceAdapter implements WetellSourcePort
{
    /**
     * @param string $endpoint    JSON-API Endpoint des Headless-Servers.
     * @param string $username    Optional Benutzername fuer Basisflow.
     * @param string $password    Optional Passwort.
     * @param string $secret      Optional shared secret.
     */
    public function __construct(
        private string $endpoint,
        private string $username,
        private string $password,
        private string $secret = ''
    ) {
    }

    /**
     * @inheritDoc
     */
    public function login(string $username, string $password): void
    {
        $response = $this->call('login', ['username' => $username, 'password' => $password]);
        if (!($response['ok'] ?? false)) {
            throw new \RuntimeException('Headless-Login fehlgeschlagen');
        }
    }

    /**
     * @inheritDoc
     */
    public function discoverInvoices(): array
    {
        $response = $this->call('discover', [
            'username' => $this->username,
            'password' => $this->password,
        ]);
        if (!($response['ok'] ?? false)) {
            throw new \RuntimeException('Headless-Discover fehlgeschlagen');
        }

        if (is_array($response['invoices'] ?? null) && count($response['invoices']) > 0) {
            $invoices = [];
            foreach ($response['invoices'] as $item) {
                if (!is_array($item)) {
                    continue;
                }

                $invoices[] = new SourceInvoice(
                    (string) ($item['sourceId'] ?? sha1((string) ($item['downloadUrl'] ?? ''))),
                    (string) ($item['vendorName'] ?? 'unbekannt'),
                    (string) ($item['downloadUrl'] ?? ''),
                    $item['invoiceDate'] ?? null,
                    $item['invoiceNumber'] ?? null
                );
            }

            return $invoices;
        }

        if (is_string($response['html'] ?? null)) {
            return $this->fallbackHtmlExtract((string) $response['html']);
        }

        return [];
    }

    /**
     * @inheritDoc
     */
    public function downloadInvoicePdf(string $downloadUrl): string
    {
        $response = $this->call('download', ['url' => $downloadUrl]);
        if (!($response['ok'] ?? false)) {
            throw new \RuntimeException('Headless-Download fehlgeschlagen');
        }

        if (is_string($response['filePath'] ?? null) && is_file((string) $response['filePath'])) {
            return (string) $response['filePath'];
        }

        $pdfBase64 = $response['pdfBase64'] ?? null;
        if (!is_string($pdfBase64)) {
            if (is_array($pdfBase64) || $pdfBase64 === null) {
                throw new \RuntimeException('Headless-PDF-Datensatz fehlte');
            }
            throw new \RuntimeException('Headless-PDF-Datensatz ungültig');
        }

        $binary = base64_decode($pdfBase64, true);
        if ($binary === false) {
            throw new \RuntimeException('Headless-PDF Base64 nicht decodierbar');
        }

        $tmp = tempnam(sys_get_temp_dir(), 'wetell_headless_');
        if ($tmp === false) {
            throw new \RuntimeException('Headless-Download: tempnam fehlgeschlagen');
        }

        $pdfPath = $tmp . '.pdf';
        if (file_put_contents($pdfPath, $binary, LOCK_EX) === false) {
            throw new \RuntimeException('Headless-Download: PDF konnte nicht gespeichert werden');
        }

        return $pdfPath;
    }

    /**
     * @param string $action Aktion.
     * @param array  $payload Daten.
     * @return array Decodierte JSON-Antwort.
     */
    private function call(string $action, array $payload): array
    {
        $payload['action'] = $action;
        if ($this->secret !== '') {
            $payload['secret'] = $this->secret;
        }

        $ch = curl_init($this->endpoint);
        if ($ch === false) {
            return ['ok' => false];
        }

        $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Accept: application/json',
            ],
            CURLOPT_POSTFIELDS => $json,
            CURLOPT_TIMEOUT => 60,
        ]);

        $response = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response === false || $httpCode >= 400) {
            return [
                'ok' => false,
                'error' => 'http_error',
                'status' => $httpCode,
            ];
        }

        $decoded = json_decode($response, true);
        if (!is_array($decoded)) {
            return [
                'ok' => false,
                'error' => 'invalid_json',
                'body' => is_string($response) ? $response : '',
            ];
        }

        return $decoded;
    }

    /**
     * @param string $html HTML.
     * @return list<SourceInvoice> fallback parse.
     */
    private function fallbackHtmlExtract(string $html): array
    {
        if (!preg_match_all('/href=["\']([^"\']+\.pdf[^"\']*)/i', $html, $matches)) {
            return [];
        }

        $result = [];
        foreach ($matches[1] as $link) {
            $result[] = new SourceInvoice(sha1($link), 'unbekannt', $link);
        }

        return $result;
    }
}