false, 'error' => 'Method Not Allowed']); } $jsonInput = file_get_contents('php://input'); if ($jsonInput === false || trim($jsonInput) === '') { json_response(400, ['ok' => false, 'error' => 'Empty payload']); } try { $data = json_decode($jsonInput, 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']); } $required = ['products', 'totalPrice', 'orderDate', 'paymentMethod', 'billing']; foreach ($required as $field) { if (!array_key_exists($field, $data)) { json_response(422, ['ok' => false, 'error' => "Missing field: {$field}"]); } } $products = $data['products']; $totalPrice = parse_number($data['totalPrice']); $orderDate = trim((string) $data['orderDate']); $paymentMethodCode = trim((string) $data['paymentMethod']); $billing = is_array($data['billing']) ? $data['billing'] : []; if (!is_array($products) || count($products) === 0) { json_response(422, ['ok' => false, 'error' => 'No products specified']); } if ($totalPrice === null || $totalPrice <= 0) { json_response(422, ['ok' => false, 'error' => 'Invalid total price']); } $orderDateTime = DateTimeImmutable::createFromFormat('!Y-m-d', $orderDate); $orderDateErrors = DateTimeImmutable::getLastErrors(); if ($orderDateTime === false || $orderDateErrors === false || $orderDateErrors['warning_count'] > 0 || $orderDateErrors['error_count'] > 0) { json_response(422, ['ok' => false, 'error' => 'Invalid order date']); } $resolvedProducts = []; $totalQty = 0.0; foreach ($products as $product) { if (!is_array($product) || !isset($product['qty'], $product['productId'])) { json_response(422, ['ok' => false, 'error' => 'Product missing productId or qty']); } $qty = parse_number($product['qty']); $productId = (int) $product['productId']; if ($qty === null || $qty <= 0 || $productId <= 0) { json_response(422, ['ok' => false, 'error' => 'Invalid product quantity or productId']); } $resolvedProducts[] = [ 'qty' => (float) $qty, 'productId' => $productId, ]; $totalQty += (float) $qty; } if ($totalQty <= 0) { json_response(422, ['ok' => false, 'error' => 'No product quantity specified']); } try { $env = expand_env_values(parse_env_file(__DIR__ . '/../../../../.env')); $pdo = connect_database($env); $pdo->beginTransaction(); $locations = get_default_location_ids($pdo); $paymentMethodId = lookup_method_id($pdo, 'payment_method', $paymentMethodCode); if ($paymentMethodId === null) { throw new RuntimeException('Invalid payment method'); } $partyId = find_or_create_party($pdo, $billing); upsert_addresses($pdo, $partyId, $billing); $order = create_direct_sales_order($pdo, [ ':party_id' => $partyId, ':payment_method_id' => $paymentMethodId, ':amount_net' => $totalPrice, ':total_amount' => $totalPrice, ':order_date' => $orderDateTime->format('Y-m-d H:i:s'), ]); $orderId = (int) $order['id']; $externalRef = (string) $order['external_ref']; $remainingTotal = round((float) $totalPrice, 2); $lineNo = 0; foreach ($resolvedProducts as $product) { $lineNo++; $qty = (float) $product['qty']; $productId = (int) $product['productId']; $productStmt = $pdo->prepare( "SELECT id, sku, name FROM product WHERE id = :id AND status = 'active' LIMIT 1" ); $productStmt->execute([':id' => $productId]); $resolvedProduct = $productStmt->fetch(); if (!is_array($resolvedProduct)) { throw new RuntimeException("Kein aktives ERP-Produkt fuer Produkt-ID {$productId} gefunden"); } $sellableItemId = find_sellable_item_for_product($pdo, $productId); if ($sellableItemId === null) { throw new RuntimeException("Kein Artikel-Mapping fuer Produkt-ID {$productId} gefunden"); } if ($lineNo === count($resolvedProducts)) { $unitPrice = round($remainingTotal / $qty, 4); $lineTotal = $remainingTotal; } else { $unitPrice = round((float) $totalPrice / (float) $totalQty, 4); $lineTotal = round($qty * $unitPrice, 2); $remainingTotal = round($remainingTotal - $lineTotal, 2); } $lineId = insert_sales_order_line($pdo, [ ':sales_order_id' => $orderId, ':line_no' => $lineNo, ':sellable_item_id' => $sellableItemId, ':article_number' => (string) $resolvedProduct['sku'], ':title' => (string) $resolvedProduct['name'], ':qty' => $qty, ':unit_price' => $unitPrice, ':line_total' => $lineTotal, ]); allocate_line_inventory( $pdo, $orderId, $lineId, $lineNo, $qty, $sellableItemId, $locations ); } $pdo->commit(); $excelTrigger = dispatch_order_import_webhooks($pdo, $env); json_response(201, [ 'ok' => true, 'orderId' => $orderId, 'externalRef' => $externalRef, 'partyId' => $partyId, 'excelTrigger' => $excelTrigger, 'message' => 'OTC order created successfully', ]); } catch (Throwable $e) { if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { $pdo->rollBack(); } json_response(500, [ 'ok' => false, 'error' => 'Internal server error: ' . $e->getMessage(), ]); }