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', 'orderDate', 'payments', 'billing']; foreach ($required as $field) { if (!array_key_exists($field, $data)) { json_response(422, ['ok' => false, 'error' => "Missing field: {$field}"]); } } $products = $data['products']; $orderDate = trim((string) $data['orderDate']); $payments = $data['payments']; $billing = is_array($data['billing']) ? $data['billing'] : []; $marketOccurrenceId = $data['marketOccurrenceId'] ?? null; $marketOccurrenceDayId = $data['marketOccurrenceDayId'] ?? null; if (($marketOccurrenceId === null) !== ($marketOccurrenceDayId === null)) { json_response(422, ['ok' => false, 'error' => 'Markt und Markttag müssen gemeinsam angegeben werden.']); } if ($marketOccurrenceId !== null && (!ctype_digit((string) $marketOccurrenceId) || (int) $marketOccurrenceId <= 0)) { json_response(422, ['ok' => false, 'error' => 'Ungültiger Markt.']); } if ($marketOccurrenceDayId !== null && (!ctype_digit((string) $marketOccurrenceDayId) || (int) $marketOccurrenceDayId <= 0)) { json_response(422, ['ok' => false, 'error' => 'Ungültiger Markttag.']); } if (!is_array($products) || count($products) === 0) { json_response(422, ['ok' => false, 'error' => 'No products specified']); } if (!is_array($payments) || count($payments) === 0) { json_response(422, ['ok' => false, 'error' => 'No payment methods specified']); } $orderDateTime = DateTimeImmutable::createFromFormat('!Y-m-d', $orderDate); $orderDateErrors = DateTimeImmutable::getLastErrors(); if ($orderDateTime === false) { json_response(422, ['ok' => false, 'error' => 'Invalid order date']); } if (is_array($orderDateErrors) && (($orderDateErrors['warning_count'] ?? 0) > 0 || ($orderDateErrors['error_count'] ?? 0) > 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']); } $resolvedPayments = []; $paymentTotal = 0.0; foreach ($payments as $payment) { if (!is_array($payment) || !isset($payment['paymentMethod'], $payment['amount'])) { json_response(422, ['ok' => false, 'error' => 'Payment missing paymentMethod or amount']); } $paymentMethodCode = trim((string) $payment['paymentMethod']); $amount = parse_number($payment['amount']); if ($paymentMethodCode === '' || $amount === null || $amount <= 0) { json_response(422, ['ok' => false, 'error' => 'Invalid payment method or amount']); } $resolvedPayments[] = [ 'paymentMethodCode' => $paymentMethodCode, 'amount' => round((float) $amount, 2), ]; $paymentTotal = round($paymentTotal + (float) $amount, 2); } if ($paymentTotal <= 0) { json_response(422, ['ok' => false, 'error' => 'Payment total must be greater than zero']); } $totalPrice = $paymentTotal; try { $env = expand_env_values(parse_env_file(__DIR__ . '/../../../../.env')); $pdo = connect_database($env); $pdo->beginTransaction(); if ($marketOccurrenceId !== null) { $marketLinkStmt = $pdo->prepare( 'SELECT 1 FROM market_occurrence o INNER JOIN market_definition m ON m.id = o.market_definition_id INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id WHERE o.id = :occurrence_id AND d.id = :day_id AND m.archived_at IS NULL AND o.archived_at IS NULL LIMIT 1' ); $marketLinkStmt->execute([ ':occurrence_id' => (int) $marketOccurrenceId, ':day_id' => (int) $marketOccurrenceDayId, ]); if ($marketLinkStmt->fetchColumn() === false) { throw new InvalidArgumentException('Der Markt oder der Markttag ist nicht aktiv oder nicht miteinander verknüpft.'); } } $locations = get_default_location_ids($pdo); $paymentMethodIds = []; foreach ($resolvedPayments as $payment) { $paymentMethodId = lookup_method_id($pdo, 'payment_method', $payment['paymentMethodCode']); if ($paymentMethodId === null) { throw new RuntimeException('Invalid payment method'); } $paymentMethodIds[] = $paymentMethodId; } $partyId = find_or_create_party($pdo, $billing); upsert_addresses($pdo, $partyId, $billing); $order = create_direct_sales_order($pdo, [ ':party_id' => $partyId, ':payment_method_id' => count($paymentMethodIds) === 1 ? $paymentMethodIds[0] : null, ':amount_net' => $totalPrice, ':total_amount' => $totalPrice, ':order_date' => $orderDateTime->format('Y-m-d H:i:s'), ':market_occurrence_id' => $marketOccurrenceId === null ? null : (int) $marketOccurrenceId, ':market_occurrence_day_id' => $marketOccurrenceDayId === null ? null : (int) $marketOccurrenceDayId, ]); $orderId = (int) $order['id']; $externalRef = (string) $order['external_ref']; $paymentStmt = $pdo->prepare( 'INSERT INTO sales_order_payment (sales_order_id, payment_method_id, amount, created_at, updated_at) VALUES (:sales_order_id, :payment_method_id, :amount, NOW(), NOW())' ); foreach ($resolvedPayments as $index => $payment) { $paymentStmt->execute([ ':sales_order_id' => $orderId, ':payment_method_id' => $paymentMethodIds[$index], ':amount' => $payment['amount'], ]); } $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 (InvalidArgumentException $e) { if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { $pdo->rollBack(); } json_response(422, [ 'ok' => false, 'error' => $e->getMessage(), ]); } catch (Throwable $e) { if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { $pdo->rollBack(); } json_response(500, [ 'ok' => false, 'error' => 'Internal server error: ' . $e->getMessage(), ]); }