false, 'error' => 'No data provided']); exit; } try { $pdo = db(); $pdo->beginTransaction(); // Validate order_type against allowed ENUM values $allowed_types = ['dine-in', 'takeaway', 'delivery', 'drive-thru']; $order_type = isset($data['order_type']) && in_array($data['order_type'], $allowed_types) ? $data['order_type'] : 'dine-in'; // Get outlet_id from input, default to 1 if missing $outlet_id = !empty($data['outlet_id']) ? intval($data['outlet_id']) : 1; $table_id = null; $table_number = null; if ($order_type === 'dine-in') { $tid = $data['table_id'] ?? ($data['table_number'] ?? null); // Support both table_id and table_number as numeric ID if ($tid) { // Validate table exists AND belongs to the correct outlet // Using standard aliases without backticks for better compatibility $stmt = $pdo->prepare( "SELECT t.id, t.table_number FROM tables t JOIN areas a ON t.area_id = a.id WHERE t.id = ? AND a.outlet_id = ?" ); $stmt->execute([$tid, $outlet_id]); $table = $stmt->fetch(PDO::FETCH_ASSOC); if ($table) { $table_id = $table['id']; $table_number = $table['table_number']; } } // If not found or not provided, leave null (Walk-in/Counter) or try to find a default table for this outlet if (!$table_id) { // Optional: try to find the first available table for this outlet $stmt = $pdo->prepare( "SELECT t.id, t.table_number FROM tables t JOIN areas a ON t.area_id = a.id WHERE a.outlet_id = ? LIMIT 1" ); $stmt->execute([$outlet_id]); $table = $stmt->fetch(PDO::FETCH_ASSOC); if ($table) { $table_id = $table['id']; $table_number = $table['table_number']; } } } // Customer Handling $customer_id = !empty($data['customer_id']) ? intval($data['customer_id']) : null; $customer_name = $data['customer_name'] ?? null; $customer_phone = $data['customer_phone'] ?? null; // Fetch Loyalty Settings $settingsStmt = $pdo->query("SELECT is_enabled, points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1"); $loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC); $loyalty_enabled = $loyaltySettings ? (bool)$loyaltySettings['is_enabled'] : true; $points_threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70; $points_per_product = $loyaltySettings ? intval($loyaltySettings['points_per_order']) : 10; $current_points = 0; $points_deducted = 0; $points_awarded = 0; $award_history_id = null; $redeem_history_id = null; if ($customer_id) { $stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?"); $stmt->execute([$customer_id]); $cust = $stmt->fetch(PDO::FETCH_ASSOC); if ($cust) { $customer_name = $cust['name']; $customer_phone = $cust['phone']; $current_points = intval($cust['points']); } else { $customer_id = null; } } // Loyalty Redemption Logic (initial check) $redeem_loyalty = !empty($data['redeem_loyalty']) && $loyalty_enabled; if ($redeem_loyalty && $customer_id) { if ($current_points < $points_threshold) { throw new Exception("Insufficient loyalty points for redemption."); } } // Total amount will be recalculated on server to be safe $calculated_subtotal = 0; $calculated_vat = 0; // First, process items to calculate real total and handle loyalty $processed_items = []; $loyalty_items_indices = []; if (!empty($data['items']) && is_array($data['items'])) { foreach ($data['items'] as $item) { $pid = $item['product_id'] ?? ($item['id'] ?? null); if (!$pid) continue; $qty = intval($item['quantity'] ?? 1); $vid = $item['variant_id'] ?? null; // Fetch Product Price (Promo aware) $pStmt = $pdo->prepare("SELECT * FROM products WHERE id = ?"); $pStmt->execute([$pid]); $product = $pStmt->fetch(PDO::FETCH_ASSOC); if (!$product) continue; $unit_price = get_product_price($product); $vat_percent = floatval($product['vat_percent'] ?? 0); $variant_name = null; // Add variant adjustment if ($vid) { $vStmt = $pdo->prepare("SELECT name, price_adjustment FROM product_variants WHERE id = ? AND product_id = ?"); $vStmt->execute([$vid, $pid]); $variant = $vStmt->fetch(PDO::FETCH_ASSOC); if ($variant) { $unit_price += floatval($variant['price_adjustment']); $variant_name = $variant['name']; } } if ($product['is_loyalty']) { $loyalty_items_indices[] = count($processed_items); } $processed_items[] = [ 'product_id' => $pid, 'product_name' => $product['name'], 'variant_id' => $vid, 'variant_name' => $variant_name, 'quantity' => $qty, 'unit_price' => $unit_price, 'vat_percent' => $vat_percent, 'is_loyalty' => (bool)$product['is_loyalty'] ]; } } // Handle Loyalty Redemption (Make loyalty items free up to points limit) if ($redeem_loyalty && $customer_id) { if (empty($loyalty_items_indices)) { throw new Exception("No loyalty-eligible products in the order to redeem."); } $total_loyalty_qty = 0; foreach ($processed_items as $item) { // Safety check: Redemption orders should only contain loyalty items as per JS logic if (!$item['is_loyalty']) { throw new Exception("Loyalty redemption orders can only contain loyalty-eligible products. Please remove non-eligible items."); } $total_loyalty_qty += $item['quantity']; } $possible_redemptions = floor($current_points / $points_threshold); if ($total_loyalty_qty > $possible_redemptions) { throw new Exception("You are only eligible for $possible_redemptions free product(s). Please reduce quantity in cart."); } $redemptions_done = 0; while ($redemptions_done < $possible_redemptions) { // Find the most expensive loyalty item that is still paid $max_price = -1; $max_index = -1; foreach ($processed_items as $idx => $item) { if ($item['is_loyalty'] && $item['unit_price'] > 0 && $item['quantity'] > 0) { if ($item['unit_price'] > $max_price) { $max_price = $item['unit_price']; $max_index = $idx; } } } if ($max_index === -1) break; // No more loyalty items to redeem // Deduct points for ONE redemption $points_deducted += $points_threshold; $redemptions_done++; // Make ONE unit of the found product free if ($processed_items[$max_index]['quantity'] > 1) { $processed_items[$max_index]['quantity']--; $free_item = $processed_items[$max_index]; $free_item['quantity'] = 1; $free_item['unit_price'] = 0; $processed_items[] = $free_item; } else { $processed_items[$max_index]['unit_price'] = 0; } } if ($redemptions_done > 0) { // Update customer points and count $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); $deductStmt->execute([$points_deducted, $customer_id]); $pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + ? WHERE id = ?") ->execute([$redemptions_done, $customer_id]); // Record Loyalty History (Deduction) $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product(s)')"); $historyStmt->execute([$customer_id, -$points_deducted]); $redeem_history_id = $pdo->lastInsertId(); // --- OVERRIDE PAYMENT TYPE --- $ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1"); $ptStmt->execute(); $loyaltyPt = $ptStmt->fetchColumn(); if ($loyaltyPt) { $data['payment_type_id'] = $loyaltyPt; } } else { throw new Exception("No loyalty-eligible products in the order to redeem."); } } // Recalculate Subtotal, VAT and Earned Points foreach ($processed_items as &$pi) { $item_subtotal = $pi['unit_price'] * $pi['quantity']; $item_vat = $item_subtotal * ($pi['vat_percent'] / 100); $pi['vat_amount'] = $item_vat; $calculated_subtotal += $item_subtotal; $calculated_vat += $item_vat; // Award points for PAID loyalty items if ($pi['is_loyalty'] && $pi['unit_price'] > 0 && $pi['quantity'] > 0) { $points_awarded += $pi['quantity'] * $points_per_product; } } unset($pi); // Award Points if ($customer_id && $loyalty_enabled && $points_awarded > 0) { $awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?"); $awardStmt->execute([$points_awarded, $customer_id]); // Record Loyalty History (Award) $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Products')"); $historyStmt->execute([$customer_id, $points_awarded]); $award_history_id = $pdo->lastInsertId(); } $final_total = max(0, $calculated_subtotal + $calculated_vat); // Explicitly ensure loyalty redemption orders have 0 total if ($redeem_loyalty) { $final_total = 0; $calculated_vat = 0; } // User/Payment info $user = get_logged_user(); $user_id = $user ? $user['id'] : null; $payment_type_id = !empty($data['payment_type_id']) ? intval($data['payment_type_id']) : null; // Commission Calculation $companySettings = get_company_settings(); $commission_amount = 0; if (!empty($companySettings['commission_enabled']) && $user_id) { $userStmt = $pdo->prepare("SELECT commission_rate FROM users WHERE id = ?"); $userStmt->execute([$user_id]); $commission_rate = (float)$userStmt->fetchColumn(); if ($commission_rate > 0) { $commission_amount = $calculated_subtotal * ($commission_rate / 100); } } // Check for Existing Order ID (Update Mode) $order_id = isset($data['order_id']) ? intval($data['order_id']) : null; $is_update = false; if ($order_id) { $checkStmt = $pdo->prepare("SELECT id FROM orders WHERE id = ?"); $checkStmt->execute([$order_id]); if ($checkStmt->fetch()) { $is_update = true; } else { $order_id = null; } } if ($is_update) { $stmt = $pdo->prepare("UPDATE orders SET outlet_id = ?, table_id = ?, table_number = ?, order_type = ?, customer_id = ?, customer_name = ?, customer_phone = ?, payment_type_id = ?, total_amount = ?, discount = ?, vat = ?, user_id = ?, commission_amount = ?, status = 'pending' WHERE id = ?"); $stmt->execute([ $outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $final_total, 0, $calculated_vat, $user_id, $commission_amount, $order_id ]); $delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?"); $delStmt->execute([$order_id]); } else { $stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, vat, user_id, commission_amount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')"); $stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $final_total, 0, $calculated_vat, $user_id, $commission_amount]); $order_id = $pdo->lastInsertId(); } // Update loyalty history with order_id if ($order_id) { if ($award_history_id) { $pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $award_history_id]); } if ($redeem_history_id) { $pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $redeem_history_id]); } } // Insert Items and Update Stock $item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, product_name, variant_id, variant_name, quantity, unit_price, vat_percent, vat_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stock_stmt = $pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?"); $order_items_list = []; foreach ($processed_items as $pi) { $item_stmt->execute([$order_id, $pi['product_id'], $pi['product_name'], $pi['variant_id'], $pi['variant_name'], $pi['quantity'], $pi['unit_price'], $pi['vat_percent'], $pi['vat_amount']]); // Decrement Stock $stock_stmt->execute([$pi['quantity'], $pi['product_id']]); $pName = $pi['product_name']; if ($pi['variant_name']) { $pName .= " ({$pi['variant_name']})"; } $order_items_list[] = "{$pi['quantity']} x $pName"; } $pdo->commit(); // --- Post-Transaction Actions (WhatsApp) --- if ($customer_id && $customer_phone) { try { $final_points = $current_points - $points_deducted + ($loyalty_enabled ? $points_awarded : 0); $wablas = new WablasService($pdo); $company_name = $companySettings['company_name'] ?? 'Flatlogic POS'; $currency_symbol = $companySettings['currency_symbol'] ?? 'OMR'; $currency_position = $companySettings['currency_position'] ?? 'after'; $stmt = $pdo->prepare("SELECT setting_value FROM integration_settings WHERE provider = 'wablas' AND setting_key = 'order_template'"); $stmt->execute(); $template = $stmt->fetchColumn(); if (!$template) { $template = "Dear *{customer_name}*, Thank you for dining with *{company_name}*! 🍽️ *Order Details:* {order_details} Total: *{total_amount}* You've earned *{points_earned} points* with this order. 💰 *Current Balance: {new_balance} points* {loyalty_status}"; } $loyalty_status = ""; if ($loyalty_enabled) { $loyalty_status = ($final_points >= $points_threshold) ? "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!" : "You need *" . ($points_threshold - $final_points) . " more points* to unlock a free meal."; } $formatted_total = number_format($final_total, (int)($companySettings['currency_decimals'] ?? 2)); if ($currency_position === 'after') { $total_with_currency = $formatted_total . " " . $currency_symbol; } else { $total_with_currency = $currency_symbol . $formatted_total; } $replacements = [ '{customer_name}' => $customer_name, '{company_name}' => $company_name, '{order_id}' => $order_id, '{order_details}' => implode("\n", $order_items_list), '{total_amount}' => $total_with_currency, '{currency_symbol}' => $currency_symbol, '{points_earned}' => $loyalty_enabled ? $points_awarded : 0, '{points_redeemed}' => $points_deducted, '{new_balance}' => $final_points, '{loyalty_status}' => $loyalty_status ]; $msg = str_replace(array_keys($replacements), array_values($replacements), $template); $customer_phone = trim((string)$customer_phone); $res = $wablas->sendMessage($customer_phone, $msg); if (empty($res['success'])) { error_log("Wablas Order Send Failed for {$customer_phone}: " . ($res['message'] ?? 'Unknown')); } } catch (Exception $w) { error_log("Wablas Exception: " . $w->getMessage()); } } echo json_encode(['success' => true, 'order_id' => $order_id]); } catch (Exception $e) { if ($pdo->inTransaction()) $pdo->rollBack(); error_log("Order Error: " . $e->getMessage()); echo json_encode(['success' => false, 'error' => $e->getMessage()]); }