From 2bb3386b5daf7c584e048a8691fb53212ced311c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 25 Feb 2026 18:31:31 +0000 Subject: [PATCH] change loyalty system --- admin/loyalty.php | 14 +- admin/products.php | 17 ++- api/order.php | 136 +++++++++++------- assets/js/main.js | 8 +- .../038_add_is_loyalty_to_products.sql | 2 + db/schema.sql | 1 + pos.php | 2 +- 7 files changed, 116 insertions(+), 64 deletions(-) create mode 100644 db/migrations/038_add_is_loyalty_to_products.sql diff --git a/admin/loyalty.php b/admin/loyalty.php index c76c313..a36b1c7 100644 --- a/admin/loyalty.php +++ b/admin/loyalty.php @@ -72,11 +72,11 @@ include 'includes/header.php';
Current Configuration
- Points per Order + Points per Loyalty Product pts
- Free Meal Threshold + Free Product Threshold pts
@@ -127,7 +127,7 @@ include 'includes/header.php'; - Eligible for Free Meal + Eligible for Free Product @@ -190,14 +190,14 @@ include 'includes/header.php';
- + -
Points awarded for every completed order.
+
Points awarded for every loyalty-participating product in the order.
- + -
Threshold points required to redeem a free meal.
+
Threshold points required to redeem a free product.
+
+
+
+ +
Include this product in earning and redeeming loyalty points
+
+ +
+
Promotion Settings
diff --git a/api/order.php b/api/order.php index 6a70db6..1811e8e 100644 --- a/api/order.php +++ b/api/order.php @@ -74,7 +74,6 @@ try { $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_per_order = $loyaltySettings ? intval($loyaltySettings['points_per_order']) : 10; $points_threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70; $current_points = 0; @@ -96,58 +95,21 @@ try { } } - // Loyalty Redemption Logic + // 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."); } - // Deduct points - $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); - $pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + 1 WHERE id = ?")->execute([$customer_id]); - $deductStmt->execute([$points_threshold, $customer_id]); - $points_deducted = $points_threshold; - - // Record Loyalty History (Deduction) - $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Meal')"); - $historyStmt->execute([$customer_id, -$points_threshold]); - $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; - } - } - - // Award Points (ONLY IF NOT REDEEMING) - if ($customer_id && $loyalty_enabled && !$redeem_loyalty) { - $awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?"); - $awardStmt->execute([$points_per_order, $customer_id]); - $points_awarded = $points_per_order; - - // Record Loyalty History (Award) - $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Order')"); - $historyStmt->execute([$customer_id, $points_per_order]); - $award_history_id = $pdo->lastInsertId(); } - // 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; - - // VAT vs Discount: We repurpose the 'discount' column in the database to store VAT value. - // If it's a positive value, it's VAT. If negative, it acts as a discount (loyalty). - $vat = isset($data['vat']) ? floatval($data['vat']) : 0.00; - // Total amount will be recalculated on server to be safe $calculated_total = 0; - // First, process items to calculate real total + // 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); @@ -176,8 +138,9 @@ try { } } - $item_total = $unit_price * $qty; - $calculated_total += $item_total; + if ($product['is_loyalty']) { + $loyalty_items_indices[] = count($processed_items); + } $processed_items[] = [ 'product_id' => $pid, @@ -185,13 +148,91 @@ try { 'variant_id' => $vid, 'variant_name' => $variant_name, 'quantity' => $qty, - 'unit_price' => $unit_price + 'unit_price' => $unit_price, + 'is_loyalty' => (bool)$product['is_loyalty'] ]; } } + // Handle Loyalty Redemption (Make one loyalty item free) + if ($redeem_loyalty && $customer_id) { + if (empty($loyalty_items_indices)) { + throw new Exception("No loyalty-eligible products in the order to redeem."); + } + + // Find the most expensive loyalty item + $max_price = -1; + $max_index = -1; + foreach ($loyalty_items_indices as $idx) { + if ($processed_items[$idx]['unit_price'] > $max_price) { + $max_price = $processed_items[$idx]['unit_price']; + $max_index = $idx; + } + } + + if ($max_index !== -1) { + // Deduct points from customer + $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); + $pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + 1 WHERE id = ?")->execute([$customer_id]); + $deductStmt->execute([$points_threshold, $customer_id]); + $points_deducted = $points_threshold; + + // Record Loyalty History (Deduction) + $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product')"); + $historyStmt->execute([$customer_id, -$points_threshold]); + $redeem_history_id = $pdo->lastInsertId(); + + // Make ONE unit of the most expensive loyalty 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; + } + + // --- 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; + } + } + } + + // Recalculate Subtotal and Earned Points + foreach ($processed_items as $pi) { + $calculated_total += $pi['unit_price'] * $pi['quantity']; + // Award points for PAID loyalty items (10 points each) + if ($pi['is_loyalty'] && $pi['unit_price'] > 0) { + $points_awarded += $pi['quantity'] * 10; + } + } + + // Award Points (ONLY IF NOT REDEEMING or we can award for remaining items) + // User said "calculate these products multiply 10 points and will be calculated to the customer" + // Usually points are earned on the whole order, but here it's specifically per product. + 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(); + } + + $vat = isset($data['vat']) ? floatval($data['vat']) : 0.00; $final_total = max(0, $calculated_total + $vat); + // 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 $commission_amount = 0; $companySettings = get_company_settings(); @@ -200,7 +241,6 @@ try { $userStmt->execute([$user_id]); $commission_rate = (float)$userStmt->fetchColumn(); if ($commission_rate > 0) { - // Commission is usually calculated on the subtotal (before tax/VAT) $commission_amount = $calculated_total * ($commission_rate / 100); } } @@ -220,7 +260,6 @@ try { } if ($is_update) { - // UPDATE Existing Order $stmt = $pdo->prepare("UPDATE orders SET outlet_id = ?, table_id = ?, table_number = ?, order_type = ?, customer_id = ?, customer_name = ?, customer_phone = ?, @@ -237,7 +276,6 @@ try { $delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?"); $delStmt->execute([$order_id]); } else { - // INSERT New Order $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, 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, $vat, $user_id, $commission_amount]); $order_id = $pdo->lastInsertId(); @@ -335,4 +373,4 @@ You've earned *{points_earned} points* with this order. if ($pdo->inTransaction()) $pdo->rollBack(); error_log("Order Error: " . $e->getMessage()); echo json_encode(['success' => false, 'error' => $e->getMessage()]); -} +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index e24e61f..664a8f1 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -266,7 +266,8 @@ document.addEventListener('DOMContentLoaded', () => { loyaltySection.classList.remove('d-none'); if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = cust.points + ' pts'; if (redeemLoyaltyBtn) { - redeemLoyaltyBtn.disabled = !cust.eligible_for_free_meal; + const hasLoyaltyItem = cart.some(item => item.is_loyalty); + redeemLoyaltyBtn.disabled = !cust.eligible_for_free_meal || !hasLoyaltyItem; if (loyaltyMessage) { if (cust.eligible_for_free_meal) loyaltyMessage.innerHTML = 'Eligible for Free Meal!'; else loyaltyMessage.textContent = `${cust.points_needed || 0} pts away from a free meal.`; @@ -476,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => { addToCart({ id: product.id, name: product.name, name_ar: product.name_ar || "", price: parseFloat(product.price), base_price: parseFloat(product.price), - hasVariants: false, quantity: 1, variant_id: null, variant_name: null + hasVariants: false, quantity: 1, variant_id: null, variant_name: null, is_loyalty: parseInt(product.is_loyalty) === 1 }); } }; @@ -498,7 +499,7 @@ document.addEventListener('DOMContentLoaded', () => { addToCart({ id: product.id, name: product.name, name_ar: product.name_ar || "", price: finalPrice, base_price: parseFloat(product.price), - hasVariants: true, quantity: 1, variant_id: v.id, variant_name: v.name + hasVariants: true, quantity: 1, variant_id: v.id, variant_name: v.name, is_loyalty: parseInt(product.is_loyalty) === 1 }); variantSelectionModal.hide(); }; @@ -539,6 +540,7 @@ document.addEventListener('DOMContentLoaded', () => { function updateCart() { if (!cartItemsContainer) return; if (cart.length === 0) { + if (redeemLoyaltyBtn) redeemLoyaltyBtn.disabled = true; cartItemsContainer.innerHTML = `

${_t('cart_empty')}

`; if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0); if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0); diff --git a/db/migrations/038_add_is_loyalty_to_products.sql b/db/migrations/038_add_is_loyalty_to_products.sql new file mode 100644 index 0000000..d8362c5 --- /dev/null +++ b/db/migrations/038_add_is_loyalty_to_products.sql @@ -0,0 +1,2 @@ +-- Add is_loyalty column to products table +ALTER TABLE products ADD COLUMN is_loyalty TINYINT(1) DEFAULT 0; diff --git a/db/schema.sql b/db/schema.sql index 78d2335..39ad5e2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS products ( description TEXT, price DECIMAL(10, 2) NOT NULL, image_url VARCHAR(255), + is_loyalty TINYINT(1) DEFAULT 0, FOREIGN KEY (category_id) REFERENCES categories(id) ); diff --git a/pos.php b/pos.php index 5d18ffa..11ef67f 100644 --- a/pos.php +++ b/pos.php @@ -310,7 +310,7 @@ if (!$loyalty_settings) {
- +