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; } }