false, 'error' => 'Method Not Allowed']); } $env = parse_env_file(__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']); } if (array_is_list($data)) { if (!isset($data[0]) || !is_array($data[0])) { json_response(400, ['ok' => false, 'error' => 'Array payload must contain one order object']); } $data = $data[0]; } $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); ensure_required_tables_exist($pdo); $pdo->beginTransaction(); $locations = get_default_location_ids($pdo); $existingOrderId = find_existing_order_id($pdo, $externalRef); $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'] ?? ''))); $orderId = upsert_sales_order($pdo, [ ':external_ref' => $externalRef, ':party_id' => $partyId, ':order_source' => 'wix', ':order_status' => 'imported', ':payment_status' => 'paid', ':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), ':currency' => 'CHF', ':webhook_payload' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); $inventoryRollback = [ 'reversedMoves' => 0, 'reversedQty' => 0.0, ]; if ($existingOrderId !== null) { $inventoryRollback = reverse_existing_allocations_for_order($pdo, $existingOrderId, $locations['storage']); } delete_sales_order_lines($pdo, $orderId); $insertedLines = 0; $inventory = [ 'linesMapped' => 0, 'linesUnmapped' => 0, 'allocationCount' => 0, 'warnings' => [], ]; foreach ($lineItems as $index => $lineItem) { if (!is_array($lineItem)) { continue; } $articleNumber = trim((string) ($lineItem['artikelnummer'] ?? '')); $title = trim((string) ($lineItem['titel'] ?? '')); $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; $sellableItemId = resolve_sellable_item_id($pdo, $articleNumber, $title); $lineNo = $index + 1; $lineId = insert_sales_order_line($pdo, [ ':sales_order_id' => $orderId, ':line_no' => $lineNo, ':sellable_item_id' => $sellableItemId, ':article_number' => $articleNumber, ':title' => $title, ':qty' => $qty, ':unit_price' => $unitPrice, ':line_total' => $lineTotal, ]); $insertedLines++; if ($sellableItemId === null) { $inventory['linesUnmapped']++; $inventory['warnings'][] = "No sellable item mapping for line {$lineNo} (artikelnummer='{$articleNumber}', titel='{$title}')"; continue; } $inventory['linesMapped']++; $allocationResult = allocate_line_inventory( $pdo, $orderId, $lineId, $lineNo, (float) $qty, $sellableItemId, $locations ); if (($allocationResult['allocated'] ?? false) === true) { $inventory['allocationCount'] += (int) ($allocationResult['allocationCount'] ?? 0); } } $pdo->commit(); $labelTrigger = trigger_shipping_label_flow($data, $env); $excelTrigger = trigger_excel_webhook($externalRef, $env); json_response(200, [ 'ok' => true, 'orderId' => $orderId, 'externalRef' => $externalRef, 'lineItemsImported' => $insertedLines, 'inventory' => $inventory, 'inventoryRollback' => $inventoryRollback, 'labelTrigger' => $labelTrigger, 'excelTrigger' => $excelTrigger, ]); } 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(), ]); }