440 lines
18 KiB
PHP
440 lines
18 KiB
PHP
<?php
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '1');
|
|
header('Content-Type: application/json');
|
|
require_once __DIR__ . '/../db/config.php';
|
|
require_once __DIR__ . '/../includes/functions.php';
|
|
require_once __DIR__ . '/../includes/WablasService.php';
|
|
|
|
$input = file_get_contents('php://input');
|
|
$data = json_decode($input, true);
|
|
|
|
|
|
if (!$data) {
|
|
echo json_encode(['success' => 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()]);
|
|
}
|