Files
erp_naurua/modules/erp/import-integration/service.php
T

304 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
function derive_label_webhook_url(array $localEnv): string
{
$explicit = env_value('N8N_LABEL_WEBHOOK_URL', $localEnv);
if ($explicit !== '') {
return $explicit;
}
$legacy = env_value('N8N_OUTBOUND_URL_ADRESSE', $localEnv);
if ($legacy !== '' && str_contains(strtolower($legacy), 'adressetikette')) {
return $legacy;
}
$base = env_value('N8N_BASE_URL', $localEnv);
if ($base === '') {
return '';
}
$root = preg_replace('#/api/v1/?$#', '', rtrim($base, '/'));
if (!is_string($root) || $root === '') {
return '';
}
return $root . '/webhook/naurua_erp_adressetikette';
}
function derive_excel_webhook_url(array $localEnv): string
{
$explicit = env_value('N8N_EXCEL_WEBHOOK_URL', $localEnv);
if ($explicit !== '') {
return $explicit;
}
$legacy = env_value('N8N_OUTBOUND_URL_ADRESSE', $localEnv);
if ($legacy !== '' && str_contains(strtolower($legacy), 'excel_befuellen')) {
return $legacy;
}
$base = env_value('N8N_BASE_URL', $localEnv);
if ($base === '') {
return '';
}
$root = preg_replace('#/api/v1/?$#', '', rtrim($base, '/'));
if (!is_string($root) || $root === '') {
return '';
}
return $root . '/webhook/excel_befuellen';
}
function post_json(string $url, array $payload, array $headers = [], int $timeoutSeconds = 15): array
{
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($body === false) {
return ['ok' => false, 'status' => 0, 'body' => '', 'error' => 'Could not encode payload'];
}
$headerLines = ['Content-Type: application/json'];
foreach ($headers as $name => $value) {
if ($name === '' || $value === '') {
continue;
}
$headerLines[] = $name . ': ' . $value;
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => implode("\r\n", $headerLines),
'content' => $body,
'timeout' => $timeoutSeconds,
'ignore_errors' => true,
],
]);
$responseBody = @file_get_contents($url, false, $context);
$responseHeaders = $http_response_header ?? [];
$status = 0;
if (isset($responseHeaders[0]) && preg_match('#HTTP/\S+\s+(\d{3})#', $responseHeaders[0], $m) === 1) {
$status = (int) $m[1];
}
if ($responseBody === false) {
$responseBody = '';
}
return [
'ok' => $status >= 200 && $status < 300,
'status' => $status,
'body' => substr($responseBody, 0, 500),
'error' => ($status === 0) ? 'Request failed or timed out' : '',
];
}
function build_n8n_webhook_headers(array $localEnv): array
{
$headers = [];
$secret = env_value('N8N_WEBHOOK_SECRET', $localEnv);
if ($secret !== '') {
$headers['X-Webhook-Secret'] = $secret;
$headers['X-N8N-Secret'] = $secret;
$headers['X-API-Key'] = $secret;
$headers['Authorization'] = 'Bearer ' . $secret;
}
return $headers;
}
function trigger_shipping_label_flow(array $order, array $localEnv): array
{
$url = derive_label_webhook_url($localEnv);
if ($url === '') {
return [
'enabled' => false,
'ok' => false,
'message' => 'Label webhook URL not configured',
];
}
$headers = build_n8n_webhook_headers($localEnv);
throttle_webhook_channel('label', 10);
$result = post_json($url, $order, $headers, 20);
return [
'enabled' => true,
'ok' => $result['ok'],
'status' => $result['status'],
'url' => $url,
'message' => $result['ok'] ? 'Label webhook triggered' : ($result['error'] !== '' ? $result['error'] : 'Label webhook returned non-2xx'),
'responseBody' => $result['body'],
];
}
function dispatch_order_import_webhooks(PDO $pdo, array $localEnv, int $limit = 20): array
{
$url = derive_excel_webhook_url($localEnv);
if ($url === '') {
return [
'enabled' => false,
'ok' => false,
'processed' => 0,
'sent' => 0,
'failed' => 0,
'deadLetter' => 0,
'message' => 'Excel webhook URL not configured',
];
}
$headers = build_n8n_webhook_headers($localEnv);
$limit = max(1, min(100, $limit));
$summary = [
'enabled' => true,
'ok' => false,
'processed' => 0,
'sent' => 0,
'failed' => 0,
'deadLetter' => 0,
'status' => 0,
'url' => $url,
'message' => 'No outbound order.imported event queued',
'responseBody' => '',
];
try {
for ($i = 0; $i < $limit; $i++) {
$pdo->beginTransaction();
$stmt = $pdo->query(
"SELECT id, payload, attempt_count
FROM outbound_webhook_event
WHERE event_type = 'order.imported'
AND status IN ('pending', 'failed')
AND next_attempt_at <= NOW()
ORDER BY created_at ASC, id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED"
);
$event = $stmt !== false ? $stmt->fetch(PDO::FETCH_ASSOC) : false;
if (!is_array($event)) {
$pdo->commit();
break;
}
$eventId = (int) $event['id'];
$attemptCount = max(0, (int) $event['attempt_count']) + 1;
$payloadRaw = $event['payload'];
$payload = [];
if (is_string($payloadRaw) && $payloadRaw !== '') {
try {
$payload = json_decode($payloadRaw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
$payload = [];
}
} elseif (is_array($payloadRaw)) {
$payload = $payloadRaw;
}
$externalRef = trim((string) ($payload['externalRef'] ?? ''));
if ($externalRef === '') {
$update = $pdo->prepare(
"UPDATE outbound_webhook_event
SET status = 'dead_letter',
last_attempt_at = NOW(),
last_error = :last_error,
next_attempt_at = NOW(),
attempt_count = :attempt_count
WHERE id = :id"
);
$update->execute([
':last_error' => 'Missing externalRef in outbound payload',
':attempt_count' => $attemptCount,
':id' => $eventId,
]);
$pdo->commit();
$summary['processed']++;
$summary['failed']++;
$summary['deadLetter']++;
$summary['ok'] = false;
$summary['message'] = 'Outbound payload missing externalRef';
continue;
}
throttle_webhook_channel('excel', 5);
$result = post_json($url, ['Bestellnummer' => $externalRef], $headers, 20);
$summary['processed']++;
$summary['status'] = $result['status'];
$summary['responseBody'] = $result['body'];
if ($result['ok']) {
$update = $pdo->prepare(
"UPDATE outbound_webhook_event
SET status = 'sent',
last_attempt_at = NOW(),
last_error = NULL,
sent_at = NOW(),
next_attempt_at = NOW(),
attempt_count = :attempt_count
WHERE id = :id"
);
$update->execute([
':attempt_count' => $attemptCount,
':id' => $eventId,
]);
$pdo->commit();
$summary['sent']++;
if ($summary['failed'] === 0) {
$summary['ok'] = true;
}
$summary['message'] = 'Excel webhook triggered';
continue;
}
$backoffSeconds = min(3600, 60 * (2 ** max(0, $attemptCount - 1)));
$status = $attemptCount >= 5 ? 'dead_letter' : 'failed';
$nextAttemptAt = (new DateTimeImmutable('now'))
->modify('+' . $backoffSeconds . ' seconds')
->format('Y-m-d H:i:s');
$lastError = $result['error'] !== '' ? $result['error'] : ('HTTP ' . $result['status']);
$update = $pdo->prepare(
"UPDATE outbound_webhook_event
SET status = :status,
last_attempt_at = NOW(),
last_error = :last_error,
next_attempt_at = :next_attempt_at,
attempt_count = :attempt_count
WHERE id = :id"
);
$update->execute([
':status' => $status,
':last_error' => $lastError,
':next_attempt_at' => $nextAttemptAt,
':attempt_count' => $attemptCount,
':id' => $eventId,
]);
$pdo->commit();
$summary['failed']++;
$summary['ok'] = false;
if ($status === 'dead_letter') {
$summary['deadLetter']++;
}
$summary['message'] = $lastError;
}
return $summary;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$summary['ok'] = false;
$summary['message'] = $e->getMessage();
return $summary;
}
}