Files
erp_naurua/modules/erp/direktverkauf/api/otc-order.php
T

280 lines
9.8 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../../modules/shared/db.php';
require_once __DIR__ . '/../../../../modules/shared/webhook_throttle.php';
require_once __DIR__ . '/../../kontakte/service.php';
require_once __DIR__ . '/../../bestellungen/service.php';
require_once __DIR__ . '/../../artikel-mapping/service.php';
require_once __DIR__ . '/../../lager/service.php';
require_once __DIR__ . '/../../import-integration/service.php';
header('Content-Type: application/json; charset=utf-8');
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
json_response(405, ['ok' => 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(),
]);
}