401 lines
13 KiB
PHP
401 lines
13 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 trigger_excel_webhook(string $externalRef, array $localEnv): array
|
|
{
|
|
$url = derive_excel_webhook_url($localEnv);
|
|
if ($url === '') {
|
|
return ['enabled' => false, 'ok' => false, 'message' => 'Excel webhook URL not configured'];
|
|
}
|
|
|
|
throttle_webhook_channel('excel', 5);
|
|
$result = post_json($url, ['Bestellnummer' => $externalRef], build_n8n_webhook_headers($localEnv), 20);
|
|
|
|
return [
|
|
'enabled' => true,
|
|
'ok' => $result['ok'],
|
|
'status' => $result['status'],
|
|
'url' => $url,
|
|
'message' => $result['ok'] ? 'Excel webhook triggered' : ($result['error'] !== '' ? $result['error'] : 'Excel webhook returned non-2xx'),
|
|
'responseBody' => $result['body'],
|
|
];
|
|
}
|
|
|
|
function build_legacy_label_order_payload(PDO $pdo, int $orderId): array
|
|
{
|
|
$orderStmt = $pdo->prepare(
|
|
'SELECT so.*, p.email,
|
|
pm.code AS payment_method_code,
|
|
sm.code AS shipping_method_code
|
|
FROM sales_order so
|
|
JOIN party p ON p.id = so.party_id
|
|
LEFT JOIN payment_method pm ON pm.id = so.payment_method_id
|
|
LEFT JOIN shipping_method sm ON sm.id = so.shipping_method_id
|
|
WHERE so.id = :order_id'
|
|
);
|
|
$orderStmt->execute([':order_id' => $orderId]);
|
|
$order = $orderStmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!is_array($order)) {
|
|
throw new RuntimeException("ERP-Bestellung {$orderId} fehlt fuer Label-Webhook");
|
|
}
|
|
|
|
$addressStmt = $pdo->prepare(
|
|
'SELECT DISTINCT ON (type) type, first_name, last_name, street, house_number,
|
|
zip, city, state_code, country_name
|
|
FROM address
|
|
WHERE party_id = :party_id
|
|
ORDER BY type, id DESC'
|
|
);
|
|
$addressStmt->execute([':party_id' => (int) $order['party_id']]);
|
|
$addresses = [];
|
|
foreach ($addressStmt->fetchAll(PDO::FETCH_ASSOC) as $address) {
|
|
$addresses[(string) $address['type']] = $address;
|
|
}
|
|
|
|
$lineStmt = $pdo->prepare(
|
|
'SELECT raw_external_article_number, raw_external_title, qty, unit_price
|
|
FROM sales_order_line
|
|
WHERE sales_order_id = :order_id
|
|
ORDER BY line_no, id'
|
|
);
|
|
$lineStmt->execute([':order_id' => $orderId]);
|
|
$lineItems = [];
|
|
foreach ($lineStmt->fetchAll(PDO::FETCH_ASSOC) as $line) {
|
|
$lineItems[] = [
|
|
'artikelnummer' => (string) ($line['raw_external_article_number'] ?? ''),
|
|
'titel' => (string) ($line['raw_external_title'] ?? ''),
|
|
'artikelanzahl' => (float) $line['qty'],
|
|
'preisEinheit' => $line['unit_price'] === null ? null : (float) $line['unit_price'],
|
|
];
|
|
}
|
|
|
|
$address = static function (array $value, string $suffix): array {
|
|
return [
|
|
"Vorname_{$suffix}" => (string) ($value['first_name'] ?? ''),
|
|
"Nachname_{$suffix}" => (string) ($value['last_name'] ?? ''),
|
|
"Strasse_{$suffix}" => (string) ($value['street'] ?? ''),
|
|
"Hausnummer_{$suffix}" => (string) ($value['house_number'] ?? ''),
|
|
"PLZ_{$suffix}" => (string) ($value['zip'] ?? ''),
|
|
"Stadt_{$suffix}" => (string) ($value['city'] ?? ''),
|
|
"Bundesland_{$suffix}" => (string) ($value['state_code'] ?? ''),
|
|
"Land_{$suffix}" => (string) ($value['country_name'] ?? ''),
|
|
];
|
|
};
|
|
|
|
return array_merge([
|
|
'BestellungNr' => (string) $order['external_ref'],
|
|
'EmailKunde' => (string) ($order['email'] ?? ''),
|
|
'Bestelldatum' => (new DateTimeImmutable((string) $order['order_date']))->format('Y-m-d\\TH:i:s'),
|
|
'Zahlungsmethode' => (string) ($order['payment_method_code'] ?? ''),
|
|
'Zahlungsstatus' => (string) $order['payment_status'],
|
|
'Liefermethode' => (string) ($order['shipping_method_code'] ?? ''),
|
|
'Netto' => (float) ($order['amount_net'] ?? 0),
|
|
'Versandkosten' => (float) ($order['amount_shipping'] ?? 0),
|
|
'Mehrwertsteuer' => (float) ($order['amount_tax'] ?? 0),
|
|
'Rabatt' => (float) ($order['amount_discount'] ?? 0),
|
|
'Gesamtsumme' => (float) ($order['total_amount'] ?? 0),
|
|
'lineItems' => $lineItems,
|
|
], $address($addresses['shipping'] ?? [], 'LfAdr'), $address($addresses['billing'] ?? [], 'RgAdr'));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|