← Code-Übersicht

HeadlessWetellSourceAdapter.php

Pfad: src/Infrastructure/Rechnung/HeadlessWetellSourceAdapter.php

Ext: php

Größe: 4664 Bytes

Geändert: 2026-07-16 18:20:06+02

<?php

declare (strict_types=1);
namespace Demo\Infrastructure\Rechnung;

use Demo\Domain\Rechnung\SourceInvoice;
use Demo\Domain\Rechnung\WetellSourcePort;
use JsonException;
use RuntimeException;
/**
 * Authentisierter Client fuer den lokalen Playwright-Service.
 */
final class HeadlessWetellSourceAdapter implements WetellSourcePort
{
    private string $username;
    private string $password;
    private readonly WetellUrlPolicy $policy;
    /** Bindet Endpoint, Portalidentitaet und Pflichtsecret. */
    public function __construct(private readonly string $endpoint, string $username, string $password, private readonly string $secret = '')
    {
        $host = parse_url($endpoint, PHP_URL_HOST);
        if (!in_array($host, ['127.0.0.1', 'localhost', '::1'], true) || $secret === '') {
            throw new RuntimeException('Headless-Service muss lokal und mit Secret konfiguriert sein');
        }
        $this->username = $username;
        $this->password = $password;
        $this->policy = new WetellUrlPolicy('https://mein.wetell.de/deine-rechnungen');
    }
    /** Meldet exakt die Identitaet dieser Session an. */
    public function login(string $username, string $password): \Demo\Domain\Rechnung\WetellSessionHandle
    {
        $this->username = $username;
        $this->password = $password;
        $this->call(['action' => 'login']);
        return \Demo\Domain\Rechnung\WetellSessionHandle::issue($this);
    }
    /** @return list<SourceInvoice> Gefundene Rechnungen. */
    public function discoverInvoices(): array
    {
        $payload = $this->call(['action' => 'discover']);
        $rows = $payload['invoices'] ?? null;
        if (!is_array($rows)) {
            throw new RuntimeException('Headless-Discovery ohne Rechnungsliste');
        }
        $result = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $url = $this->policy->assert((string) ($row['downloadUrl'] ?? ''));
            $result[] = new SourceInvoice((string) ($row['sourceId'] ?? hash('sha256', $url)), (string) ($row['vendorName'] ?? 'WEtell GmbH'), $url, $this->nullable($row['invoiceDate'] ?? null), $this->nullable($row['invoiceNumber'] ?? null));
        }
        return $result;
    }
    /** Laedt PDF-Bytes ohne Vertrauen in einen Host-Dateipfad. */
    public function downloadInvoicePdf(string $downloadUrl): string
    {
        $payload = $this->call(['action' => 'download', 'downloadUrl' => $this->policy->assert($downloadUrl)]);
        $encoded = $payload['pdfBase64'] ?? null;
        $binary = is_string($encoded) ? base64_decode($encoded, true) : false;
        if ($binary === false || !str_starts_with($binary, '%PDF-')) {
            throw new RuntimeException('Headless-Download ist kein PDF');
        }
        return $binary;
    }
    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    private function call(array $payload): array
    {
        $request = [...$payload, 'username' => $this->username, 'password' => $this->password, 'secret' => $this->secret];
        $ch = curl_init($this->endpoint);
        if ($ch === false) {
            throw new RuntimeException('Headless-cURL kann nicht initialisiert werden');
        }
        curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($request, JSON_THROW_ON_ERROR), CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 5]);
        $body = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        if ($status === 401 || $status === 403) {
            throw new \Demo\Domain\Rechnung\WetellAuthenticationException('Headless-Authentifizierung abgelehnt');
        }
        if (!is_string($body) || $status < 200 || $status >= 300) {
            throw new RuntimeException('Headless-Aufruf fehlgeschlagen: HTTP ' . $status . ' ' . $error);
        }
        try {
            $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new RuntimeException('Headless-Antwort ist ungueltig', 0, $exception);
        }
        if (!is_array($decoded) || ($decoded['ok'] ?? false) !== true) {
            throw new RuntimeException('Headless-Aktion nicht bestaetigt');
        }
        return $decoded;
    }
    /** Normalisiert optionale Responsewerte. */
    private function nullable(mixed $value): ?string
    {
        return $value === null ? null : (string) $value;
    }
}

Frühere Versionen

Version Zeitpunkt Operation
60 2026-07-16 20:20:52.018118+02 UPDATE
54 2026-07-16 20:18:22.872539+02 UPDATE
39 2026-07-16 19:55:01.887995+02 UPDATE