= 2) { $first = $value[0]; $last = $value[strlen($value) - 1]; if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) { $value = substr($value, 1, -1); } } $result[$key] = $value; } return $result; } function expand_env_values(array $env): array { $expanded = $env; $pattern = '/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/'; foreach ($expanded as $key => $value) { $expanded[$key] = preg_replace_callback( $pattern, static function (array $matches) use (&$expanded): string { $lookup = $matches[1]; return (string) ($expanded[$lookup] ?? getenv($lookup) ?: ''); }, (string) $value ) ?? (string) $value; } return $expanded; } function env_value(string $key, array $localEnv, string $default = ''): string { $runtime = getenv($key); if ($runtime !== false && $runtime !== '') { return $runtime; } if (isset($localEnv[$key]) && $localEnv[$key] !== '') { return (string) $localEnv[$key]; } return $default; } function parse_number(mixed $value): ?float { if ($value === null || $value === '') { return null; } if (is_int($value) || is_float($value)) { return (float) $value; } if (!is_string($value)) { return null; } $normalized = str_replace(["\u{00A0}", ' '], '', trim($value)); $normalized = str_replace(',', '.', $normalized); if (!is_numeric($normalized)) { return null; } return (float) $normalized; } function lookup_method_id(PDO $pdo, string $table, ?string $code): ?int { if ($code === null || $code === '') { return null; } $stmt = $pdo->prepare("SELECT id FROM {$table} WHERE code = :code LIMIT 1"); $stmt->execute([':code' => $code]); $id = $stmt->fetchColumn(); return $id === false ? null : (int) $id; } function map_payment_code(string $input): ?string { $v = strtolower(trim($input)); if ($v === '') { return null; } if (str_contains($v, 'twint')) { return 'twint'; } if (str_contains($v, 'bank') || str_contains($v, 'vorauskasse') || str_contains($v, 'ueberweisung')) { return 'bank_transfer'; } if (str_contains($v, 'kredit') || str_contains($v, 'debit') || str_contains($v, 'card')) { return 'card'; } return null; } function map_shipping_code(string $input): ?string { $v = strtolower(trim($input)); if ($v === '') { return null; } if (str_contains($v, 'abholung') || str_contains($v, 'pickup')) { return 'pickup'; } if (str_contains($v, 'post') || str_contains($v, 'versand')) { return 'post_standard'; } return null; } function connect_database(array $localEnv): PDO { $databaseUrl = env_value('DATABASE_URL', $localEnv); if ($databaseUrl !== '') { $parts = parse_url($databaseUrl); if ($parts !== false && ($parts['scheme'] ?? '') === 'postgresql') { $host = (string) ($parts['host'] ?? ''); $port = (string) ($parts['port'] ?? '5432'); $dbName = ltrim((string) ($parts['path'] ?? ''), '/'); $user = (string) ($parts['user'] ?? ''); $pass = (string) ($parts['pass'] ?? ''); if ($host !== '' && $dbName !== '' && $user !== '') { $dsn = "pgsql:host={$host};port={$port};dbname={$dbName}"; return new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } } } $host = env_value('DB_HOST', $localEnv); $port = env_value('DB_PORT', $localEnv, '5432'); $dbName = env_value('DB_NAME', $localEnv); $user = env_value('DB_USER', $localEnv); $pass = env_value('DB_PASSWORD', $localEnv); if ($host === '' || $dbName === '' || $user === '') { throw new RuntimeException('Missing DB configuration'); } $dsn = "pgsql:host={$host};port={$port};dbname={$dbName}"; return new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } function find_or_create_party(PDO $pdo, array $data): int { $email = trim((string) ($data['EmailKunde'] ?? '')); $firstName = trim((string) ($data['Vorname_RgAdr'] ?? '')); $lastName = trim((string) ($data['Nachname_RgAdr'] ?? '')); $name = trim($firstName . ' ' . $lastName); if ($name === '') { $name = 'Online-Shop Kunde'; } if ($email !== '') { $findStmt = $pdo->prepare('SELECT id FROM party WHERE lower(email) = lower(:email) ORDER BY id ASC LIMIT 1'); $findStmt->execute([':email' => $email]); $existing = $findStmt->fetchColumn(); if ($existing !== false) { $partyId = (int) $existing; $updateStmt = $pdo->prepare('UPDATE party SET name = :name, updated_at = NOW() WHERE id = :id'); $updateStmt->execute([':id' => $partyId, ':name' => $name]); return $partyId; } } $insertStmt = $pdo->prepare( 'INSERT INTO party (type, name, email, status, created_at, updated_at) VALUES (\'customer\', :name, :email, \'active\', NOW(), NOW()) RETURNING id' ); $insertStmt->execute([ ':name' => $name, ':email' => $email !== '' ? $email : null, ]); $id = $insertStmt->fetchColumn(); if ($id === false) { throw new RuntimeException('Could not create party'); } return (int) $id; } function upsert_addresses(PDO $pdo, int $partyId, array $data): void { $delete = $pdo->prepare('DELETE FROM address WHERE party_id = :party_id AND type IN (\'billing\', \'shipping\')'); $delete->execute([':party_id' => $partyId]); $insert = $pdo->prepare( 'INSERT INTO address ( party_id, type, first_name, last_name, street, house_number, zip, city, state_code, country_name, raw_payload, created_at, updated_at ) VALUES ( :party_id, :type, :first_name, :last_name, :street, :house_number, :zip, :city, :state_code, :country_name, :raw_payload::jsonb, NOW(), NOW() )' ); $insert->execute([ ':party_id' => $partyId, ':type' => 'billing', ':first_name' => trim((string) ($data['Vorname_RgAdr'] ?? '')), ':last_name' => trim((string) ($data['Nachname_RgAdr'] ?? '')), ':street' => trim((string) ($data['Strasse_RgAdr'] ?? '')), ':house_number' => trim((string) ($data['Hausnummer_RgAdr'] ?? '')), ':zip' => trim((string) ($data['PLZ_RgAdr'] ?? '')), ':city' => trim((string) ($data['Stadt_RgAdr'] ?? '')), ':state_code' => trim((string) ($data['Bundesland_RgAdr'] ?? '')), ':country_name' => trim((string) ($data['Land_RgAdr'] ?? '')), ':raw_payload' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); $insert->execute([ ':party_id' => $partyId, ':type' => 'shipping', ':first_name' => trim((string) ($data['Vorname_LfAdr'] ?? '')), ':last_name' => trim((string) ($data['Nachname_LfAdr'] ?? '')), ':street' => trim((string) ($data['Strasse_LfAdr'] ?? '')), ':house_number' => trim((string) ($data['Hausnummer_LfAdr'] ?? '')), ':zip' => trim((string) ($data['PLZ_LfAdr'] ?? '')), ':city' => trim((string) ($data['Stadt_LfAdr'] ?? '')), ':state_code' => trim((string) ($data['Bundesland_LfAdr'] ?? '')), ':country_name' => trim((string) ($data['Land_LfAdr'] ?? '')), ':raw_payload' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); } if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { json_response(405, ['ok' => false, 'error' => 'Method Not Allowed']); } $env = parse_env_file(__DIR__ . '/.env'); $env = array_merge($env, parse_env_file(dirname(__DIR__) . '/.env')); $env = expand_env_values($env); $expectedSecret = env_value('N8N_WEBHOOK_SECRET', $env); $providedSecret = (string) ($_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? ''); if ($expectedSecret === '') { json_response(500, ['ok' => false, 'error' => 'N8N_WEBHOOK_SECRET not configured']); } if ($providedSecret === '' || !hash_equals($expectedSecret, $providedSecret)) { json_response(401, ['ok' => false, 'error' => 'Unauthorized']); } $rawPayload = file_get_contents('php://input'); if ($rawPayload === false || trim($rawPayload) === '') { json_response(400, ['ok' => false, 'error' => 'Empty payload']); } try { $data = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { json_response(400, ['ok' => false, 'error' => 'Invalid JSON payload']); } if (!is_array($data)) { json_response(400, ['ok' => false, 'error' => 'JSON object expected']); } $externalRef = trim((string) ($data['BestellungNr'] ?? '')); if ($externalRef === '') { json_response(422, ['ok' => false, 'error' => 'BestellungNr is required']); } $lineItems = $data['lineItems'] ?? []; if (!is_array($lineItems)) { $lineItems = []; } try { $pdo = connect_database($env); $pdo->beginTransaction(); $partyId = find_or_create_party($pdo, $data); upsert_addresses($pdo, $partyId, $data); $paymentMethodId = lookup_method_id($pdo, 'payment_method', map_payment_code((string) ($data['Zahlungsmethode'] ?? ''))); $shippingMethodId = lookup_method_id($pdo, 'shipping_method', map_shipping_code((string) ($data['Liefermethode'] ?? ''))); $orderStmt = $pdo->prepare( 'INSERT INTO sales_order ( external_ref, party_id, order_source, order_status, payment_status, payment_method_id, shipping_method_id, amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, webhook_payload, imported_at, created_at, updated_at ) VALUES ( :external_ref, :party_id, \'wix\', \'imported\', \'paid\', :payment_method_id, :shipping_method_id, :amount_net, :amount_shipping, :amount_tax, :amount_discount, :total_amount, \'CHF\', :webhook_payload::jsonb, NOW(), NOW(), NOW() ) ON CONFLICT (external_ref) DO UPDATE SET party_id = EXCLUDED.party_id, order_source = EXCLUDED.order_source, order_status = EXCLUDED.order_status, payment_status = EXCLUDED.payment_status, payment_method_id = EXCLUDED.payment_method_id, shipping_method_id = EXCLUDED.shipping_method_id, amount_net = EXCLUDED.amount_net, amount_shipping = EXCLUDED.amount_shipping, amount_tax = EXCLUDED.amount_tax, amount_discount = EXCLUDED.amount_discount, total_amount = EXCLUDED.total_amount, currency = EXCLUDED.currency, webhook_payload = EXCLUDED.webhook_payload, imported_at = NOW(), updated_at = NOW() RETURNING id' ); $orderStmt->execute([ ':external_ref' => $externalRef, ':party_id' => $partyId, ':payment_method_id' => $paymentMethodId, ':shipping_method_id' => $shippingMethodId, ':amount_net' => parse_number($data['Netto'] ?? null), ':amount_shipping' => parse_number($data['Versandkosten'] ?? null), ':amount_tax' => parse_number($data['Mehrwertsteuer'] ?? null), ':amount_discount' => parse_number($data['Rabatt'] ?? null), ':total_amount' => parse_number($data['Gesamtsumme'] ?? null), ':webhook_payload' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); $orderId = $orderStmt->fetchColumn(); if ($orderId === false) { throw new RuntimeException('Could not upsert order'); } $orderId = (int) $orderId; $deleteLines = $pdo->prepare('DELETE FROM sales_order_line WHERE sales_order_id = :sales_order_id'); $deleteLines->execute([':sales_order_id' => $orderId]); $lineInsert = $pdo->prepare( 'INSERT INTO sales_order_line ( sales_order_id, line_no, sellable_item_id, raw_external_article_number, raw_external_title, qty, unit_price, line_total, created_at, updated_at ) VALUES ( :sales_order_id, :line_no, NULL, :article_number, :title, :qty, :unit_price, :line_total, NOW(), NOW() )' ); $insertedLines = 0; foreach ($lineItems as $index => $lineItem) { if (!is_array($lineItem)) { continue; } $qty = parse_number($lineItem['artikelanzahl'] ?? null); if ($qty === null || $qty <= 0) { continue; } $unitPrice = parse_number($lineItem['preisEinheit'] ?? null); $lineTotal = $unitPrice !== null ? round($qty * $unitPrice, 2) : null; $lineInsert->execute([ ':sales_order_id' => $orderId, ':line_no' => $index + 1, ':article_number' => trim((string) ($lineItem['artikelnummer'] ?? '')), ':title' => trim((string) ($lineItem['titel'] ?? '')), ':qty' => $qty, ':unit_price' => $unitPrice, ':line_total' => $lineTotal, ]); $insertedLines++; } $pdo->commit(); json_response(200, [ 'ok' => true, 'orderId' => $orderId, 'externalRef' => $externalRef, 'lineItemsImported' => $insertedLines, ]); } catch (Throwable $e) { if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { $pdo->rollBack(); } json_response(500, [ 'ok' => false, 'error' => 'Order import failed', 'detail' => $e->getMessage(), ]); }