← Code-Übersicht

run-rechnung.php

Pfad: run-rechnung.php

Ext: php

Größe: 8854 Bytes

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

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

#!/usr/bin/env php
<?php
declare(strict_types=1);

use Demo\Autoloader;
use Demo\Application\Rechnung\DiscoverInvoicesUseCase;
use Demo\Application\Rechnung\DownloadInvoiceUseCase;
use Demo\Application\Rechnung\ExtractInvoiceDataUseCase;
use Demo\Application\Rechnung\IngestInvoicesUseCase;
use Demo\Application\Rechnung\PostInvoiceUseCase;
use Demo\Application\Rechnung\ResolveVendorUseCase;
use Demo\Infrastructure\Rechnung\DateBasedInvoiceStorage;
use Demo\Infrastructure\Rechnung\FallbackTextExtractor;
use Demo\Infrastructure\Rechnung\FallbackWetellSourceAdapter;
use Demo\Infrastructure\Rechnung\HeadlessWetellSourceAdapter;
use Demo\Infrastructure\Rechnung\HttpWetellSourceAdapter;
use Demo\Infrastructure\Rechnung\JsonInvoiceStateAdapter;
use Demo\Infrastructure\Rechnung\JsonLineLogger;
use Demo\Infrastructure\Rechnung\LexofficeApiAdapter;
use Demo\Infrastructure\Rechnung\PdftotextExtractor;
use Demo\Infrastructure\Rechnung\SystemClock;
use Demo\Infrastructure\Rechnung\TesseractInvoiceExtractor;

require __DIR__ . '/src/Autoloader.php';

(new Autoloader(__DIR__ . '/src'))->register();

/**
 * @param string      $file      Dateipfad.
 * @param string      $label     Label fuer Fehlermeldung.
 * @param array       $defaults  Default-Werte.
 * @return array<string,mixed> Geladene JSON-Daten.
 */
$loadJsonConfig = function (string $file, string $label, array $defaults = []): array {
    if (!is_file($file)) {
        return $defaults;
    }

    $raw = (string) file_get_contents($file);
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        throw new RuntimeException($label . ' ist kein valides JSON: ' . $file);
    }

    return $decoded;
};

$resolveString = static function (array $source, string $primary, array $aliases = [], string $default = ''): string {
    if (isset($source[$primary]) && (is_string($source[$primary]) || is_numeric($source[$primary])) && trim((string) $source[$primary]) !== '') {
        return (string) $source[$primary];
    }

    foreach ($aliases as $alias) {
        if (isset($source[$alias]) && (is_string($source[$alias]) || is_numeric($source[$alias])) && trim((string) $source[$alias]) !== '') {
            return (string) $source[$alias];
        }
    }

    return $default;
};
$toBool = static function (mixed $value): bool {
    if (is_bool($value)) {
        return $value;
    }
    if (is_int($value)) {
        return $value > 0;
    }
    if (is_string($value)) {
        return filter_var($value, FILTER_VALIDATE_BOOL) !== false;
    }

    return false;
};

$projectRoot = __DIR__;
$credentialsDir = getenv('APP_CREDENTIALS_DIR') ?: $projectRoot . '/credentials';

$wetell = $loadJsonConfig(
    getenv('WETELL_CREDENTIALS_FILE') ?: $credentialsDir . '/wetell.json',
    'wetell.json',
    [
        'loginUrl' => 'https://mein.wetell.de/anmelden',
        'invoicesUrl' => 'https://mein.wetell.de/deine-rechnungen',
        'listSelector' => '',
    ]
);

$lexoffice = $loadJsonConfig(
    getenv('LEXOFFICE_CREDENTIALS_FILE') ?: $credentialsDir . '/lexoffice.json',
    'lexoffice.json',
    [
        'baseUrl' => 'https://api.lexoffice.io',
        'apiKey' => '',
        'rateLimitPerSecond' => 2,
    ]
);

$wetell = [
    'loginUrl' => $resolveString(
        $wetell,
        'loginUrl',
        ['url_login', 'urlLogin'],
        'https://mein.wetell.de/anmelden'
    ),
    'invoicesUrl' => $resolveString(
        $wetell,
        'invoicesUrl',
        ['url_rechnungen', 'urlInvoices'],
        'https://mein.wetell.de/deine-rechnungen'
    ),
    'username' => $resolveString($wetell, 'username'),
    'password' => $resolveString($wetell, 'password'),
    'usernameField' => $resolveString($wetell, 'usernameField', [], 'username'),
    'passwordField' => $resolveString($wetell, 'passwordField', [], 'password'),
    'userAgent' => $resolveString($wetell, 'userAgent', [], 'DemoBot-Rechnung/1.0'),
    'listSelector' => $resolveString($wetell, 'listSelector', ['listselector'], ''),
    'pdftotextBinary' => $resolveString($wetell, 'pdftotextBinary', [], 'pdftotext'),
    'tesseractBinary' => $resolveString($wetell, 'tesseractBinary', [], 'tesseract'),
    'textExtractorMinConfidence' => $resolveString($wetell, 'textExtractorMinConfidence', [], '0.75'),
    'headlessEndpoint' => $resolveString($wetell, 'headlessEndpoint', ['headless_endpoint'], ''),
    'headlessUseAsPrimary' => $toBool($wetell['headlessUseAsPrimary'] ?? $wetell['headless_use_as_primary'] ?? false),
    'headlessSecret' => $resolveString($wetell, 'headlessSecret'),
];

$lexoffice = [
    'baseUrl' => $resolveString(
        $lexoffice,
        'baseUrl',
        ['url_base', 'api_base_url'],
        'https://api.lexoffice.io'
    ),
    'apiKey' => $resolveString(
        $lexoffice,
        'apiKey',
        ['api-key', 'api_key'],
        ''
    ),
    'rateLimitPerSecond' => (int) $resolveString($lexoffice, 'rateLimitPerSecond', [], '2'),
    'paths' => is_array($lexoffice['paths'] ?? null) ? $lexoffice['paths'] : [],
];

if (($wetell['username'] ?? '') === '' || ($wetell['password'] ?? '') === '') {
    throw new RuntimeException('wetell.json: username und password sind erforderlich');
}
if (($lexoffice['apiKey'] ?? '') === '') {
    throw new RuntimeException('lexoffice.json: apiKey ist erforderlich');
}

$runId = getenv('RECHNUNGEN_RUN_ID') ?: ('run_' . date('Ymd_His'));
$statePath = getenv('RECHNUNGEN_STATE_PATH') ?: $projectRoot . '/rechnungen/rechnungen.json';
$originalsBasePath = getenv('RECHNUNGEN_ORIGINAL_BASE_PATH')
    ?: $projectRoot . '/rechnungen/originale';
$logPath = getenv('RECHNUNGEN_LOG_PATH') ?: $projectRoot . '/rechnungen/prozess-rechnung.log';
$maxRetries = (int) (getenv('RECHNUNGEN_MAX_RETRIES') ?: '3');
$amountTolerance = (float) (getenv('RECHNUNGEN_AMOUNT_TOLERANCE') ?: '0.01');

$logger = new JsonLineLogger($logPath);
$clock = new SystemClock();

$statePort = new JsonInvoiceStateAdapter($statePath);
$storage = new DateBasedInvoiceStorage($originalsBasePath);

$primarySource = new HttpWetellSourceAdapter(
    (string) ($wetell['loginUrl'] ?? 'https://mein.wetell.de/anmelden'),
    (string) ($wetell['invoicesUrl'] ?? 'https://mein.wetell.de/deine-rechnungen'),
    (string) ($wetell['listSelector'] ?? ''),
    (string) ($wetell['usernameField'] ?? 'username'),
    (string) ($wetell['passwordField'] ?? 'password'),
    $logger,
    $wetell['userAgent'] ?? null
);

$headlessEndpoint = getenv('RECHNUNGEN_HEADLESS_ENDPOINT');
if ($headlessEndpoint === false) {
    $headlessEndpoint = getenv('WETELL_HEADLESS_ENDPOINT');
}
if ($headlessEndpoint === null || $headlessEndpoint === false || $headlessEndpoint === '') {
    $headlessEndpoint = $wetell['headlessEndpoint'] ?? null;
}
$headlessPrimaryEnv = getenv('RECHNUNGEN_HEADLESS_PRIMARY');
$headlessPrimary = $headlessPrimaryEnv === false
    ? (bool) ($wetell['headlessUseAsPrimary'] ?? false)
    : $toBool($headlessPrimaryEnv);
$headlessSource = null;
if ($headlessEndpoint !== null && $headlessEndpoint !== '' && $headlessEndpoint !== false) {
    $headlessSource = new HeadlessWetellSourceAdapter(
        $headlessEndpoint,
        (string) ($wetell['username'] ?? ''),
        (string) ($wetell['password'] ?? ''),
        (string) ($wetell['headlessSecret'] ?? '')
    );
}

$sourcePort = $headlessPrimary && $headlessSource !== null
    ? new FallbackWetellSourceAdapter($headlessSource, $primarySource, $logger)
    : new FallbackWetellSourceAdapter($primarySource, $headlessSource, $logger);

$extractorPrimary = new PdftotextExtractor($wetell['pdftotextBinary'] ?? 'pdftotext');
$extractorFallback = new TesseractInvoiceExtractor($wetell['tesseractBinary'] ?? 'tesseract');
$extractor = new FallbackTextExtractor(
    $extractorPrimary,
    $extractorFallback,
    (float) ($wetell['textExtractorMinConfidence'] ?? 0.75)
);

$lexofficePort = new LexofficeApiAdapter(
    (string) ($lexoffice['baseUrl'] ?? 'https://api.lexoffice.io'),
    (string) ($lexoffice['apiKey'] ?? ''),
    (int) ($lexoffice['rateLimitPerSecond'] ?? 2),
    is_array($lexoffice['paths'] ?? null) ? $lexoffice['paths'] : []
);

$discover = new DiscoverInvoicesUseCase($sourcePort, $logger);
$download = new DownloadInvoiceUseCase($sourcePort, $storage, $logger, $clock);
$extract = new ExtractInvoiceDataUseCase($extractor, $logger, $clock);
$resolveVendor = new ResolveVendorUseCase($lexofficePort, $logger);
$post = new PostInvoiceUseCase($lexofficePort, $logger);

$ingest = new IngestInvoicesUseCase(
    $statePort,
    $discover,
    $download,
    $extract,
    $resolveVendor,
    $post,
    $logger,
    $clock,
    $maxRetries,
    $amountTolerance
);

$result = $ingest->execute(
    $runId,
    (string) $wetell['username'],
    (string) $wetell['password']
);

echo json_encode($result->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;