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 |