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;