loyalty update

This commit is contained in:
Flatlogic Bot 2026-02-25 19:30:09 +00:00
parent a1d6822c0a
commit 600e86d0fa
5 changed files with 131 additions and 47 deletions

View File

@ -141,6 +141,8 @@ try {
if ($product['is_loyalty']) { if ($product['is_loyalty']) {
$loyalty_items_indices[] = count($processed_items); $loyalty_items_indices[] = count($processed_items);
} elseif ($redeem_loyalty) {
throw new Exception("Redemption is only allowed when all items in the cart are loyalty-eligible.");
} }
$processed_items[] = [ $processed_items[] = [
@ -155,35 +157,35 @@ try {
} }
} }
// Handle Loyalty Redemption (Make one loyalty item free) // Handle Loyalty Redemption (Make loyalty items free up to points limit)
if ($redeem_loyalty && $customer_id) { if ($redeem_loyalty && $customer_id) {
if (empty($loyalty_items_indices)) { if (empty($loyalty_items_indices)) {
throw new Exception("No loyalty-eligible products in the order to redeem."); throw new Exception("No loyalty-eligible products in the order to redeem.");
} }
// Find the most expensive loyalty item $possible_redemptions = floor($current_points / $points_threshold);
$redemptions_done = 0;
while ($redemptions_done < $possible_redemptions) {
// Find the most expensive loyalty item that is still paid
$max_price = -1; $max_price = -1;
$max_index = -1; $max_index = -1;
foreach ($loyalty_items_indices as $idx) { foreach ($processed_items as $idx => $item) {
if ($processed_items[$idx]['unit_price'] > $max_price) { if ($item['is_loyalty'] && $item['unit_price'] > 0 && $item['quantity'] > 0) {
$max_price = $processed_items[$idx]['unit_price']; if ($item['unit_price'] > $max_price) {
$max_price = $item['unit_price'];
$max_index = $idx; $max_index = $idx;
} }
} }
}
if ($max_index !== -1) { if ($max_index === -1) break; // No more loyalty items to redeem
// 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) // Deduct points for ONE redemption
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product')"); $points_deducted += $points_threshold;
$historyStmt->execute([$customer_id, -$points_threshold]); $redemptions_done++;
$redeem_history_id = $pdo->lastInsertId();
// Make ONE unit of the most expensive loyalty product free // Make ONE unit of the found product free
if ($processed_items[$max_index]['quantity'] > 1) { if ($processed_items[$max_index]['quantity'] > 1) {
$processed_items[$max_index]['quantity']--; $processed_items[$max_index]['quantity']--;
$free_item = $processed_items[$max_index]; $free_item = $processed_items[$max_index];
@ -193,6 +195,19 @@ try {
} else { } else {
$processed_items[$max_index]['unit_price'] = 0; $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 --- // --- OVERRIDE PAYMENT TYPE ---
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1"); $ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
@ -201,6 +216,8 @@ try {
if ($loyaltyPt) { if ($loyaltyPt) {
$data['payment_type_id'] = $loyaltyPt; $data['payment_type_id'] = $loyaltyPt;
} }
} else {
throw new Exception("No loyalty-eligible products in the order to redeem.");
} }
} }
@ -208,14 +225,12 @@ try {
foreach ($processed_items as $pi) { foreach ($processed_items as $pi) {
$calculated_total += $pi['unit_price'] * $pi['quantity']; $calculated_total += $pi['unit_price'] * $pi['quantity'];
// Award points for PAID loyalty items // Award points for PAID loyalty items
if ($pi['is_loyalty'] && $pi['unit_price'] > 0) { if ($pi['is_loyalty'] && $pi['unit_price'] > 0 && $pi['quantity'] > 0) {
$points_awarded += $pi['quantity'] * $points_per_product; $points_awarded += $pi['quantity'] * $points_per_product;
} }
} }
// Award Points (ONLY IF NOT REDEEMING or we can award for remaining items) // Award Points
// 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) { if ($customer_id && $loyalty_enabled && $points_awarded > 0) {
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?"); $awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
$awardStmt->execute([$points_awarded, $customer_id]); $awardStmt->execute([$points_awarded, $customer_id]);

View File

@ -47,9 +47,9 @@ try {
exit; exit;
} }
// Fetch Items // Fetch Items with is_loyalty
$stmtItems = $pdo->prepare(" $stmtItems = $pdo->prepare("
SELECT oi.*, p.name as product_name, p.price as base_price, v.name as variant_name, v.price_adjustment SELECT oi.*, p.name as product_name, p.price as base_price, p.is_loyalty, v.name as variant_name, v.price_adjustment
FROM order_items oi FROM order_items oi
JOIN products p ON oi.product_id = p.id JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants v ON oi.variant_id = v.id LEFT JOIN product_variants v ON oi.variant_id = v.id
@ -65,13 +65,12 @@ try {
'id' => $item['product_id'], 'id' => $item['product_id'],
'name' => $item['product_name'], 'name' => $item['product_name'],
'price' => floatval($item['unit_price']), 'price' => floatval($item['unit_price']),
'base_price' => floatval($item['base_price']), // Note: this might be different from current DB price if changed, but for recall we usually want the ORIGINAL price or CURRENT? 'base_price' => floatval($item['base_price']),
// Ideally, if we edit an order, we might want to keep original prices OR update to current.
// For simplicity, let's use the price stored in order_items (unit_price) as the effective price.
'quantity' => intval($item['quantity']), 'quantity' => intval($item['quantity']),
'variant_id' => $item['variant_id'], 'variant_id' => $item['variant_id'],
'variant_name' => $item['variant_name'], 'variant_name' => $item['variant_name'],
'hasVariants' => !empty($item['variant_id']) 'hasVariants' => !empty($item['variant_id']),
'is_loyalty' => intval($item['is_loyalty']) === 1
]; ];
} }
@ -81,6 +80,18 @@ try {
$cStmt = $pdo->prepare("SELECT * FROM customers WHERE id = ?"); $cStmt = $pdo->prepare("SELECT * FROM customers WHERE id = ?");
$cStmt->execute([$order['customer_id']]); $cStmt->execute([$order['customer_id']]);
$customer = $cStmt->fetch(PDO::FETCH_ASSOC); $customer = $cStmt->fetch(PDO::FETCH_ASSOC);
if ($customer) {
// Fetch Loyalty Threshold for consistent frontend logic
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70;
$customer['points'] = intval($customer['points']);
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
$customer['eligible_count'] = floor($customer['points'] / $threshold);
$customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold));
}
} }
echo json_encode([ echo json_encode([

View File

@ -15,7 +15,7 @@ try {
// Fetch Loyalty Settings // Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1"); $settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC); $settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? $settings['points_for_free_meal'] : 70; $threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
$stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10"); $stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10");
$searchTerm = "%$q%"; $searchTerm = "%$q%";
@ -25,7 +25,8 @@ try {
foreach ($customers as &$customer) { foreach ($customers as &$customer) {
$customer['points'] = intval($customer['points']); $customer['points'] = intval($customer['points']);
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold; $customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
$customer['points_needed'] = max(0, $threshold - $customer['points']); $customer['eligible_count'] = floor($customer['points'] / $threshold);
$customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold));
$customer['threshold'] = $threshold; $customer['threshold'] = $threshold;
} }

View File

@ -251,6 +251,39 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
function updateLoyaltyUI() {
if (!loyaltySection || typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return;
if (currentCustomer) {
loyaltySection.classList.remove('d-none');
if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = currentCustomer.points + ' pts';
if (redeemLoyaltyBtn) {
const hasLoyaltyItem = cart.some(item => item.is_loyalty);
const hasNonLoyaltyItem = cart.some(item => !item.is_loyalty);
redeemLoyaltyBtn.disabled = !currentCustomer.eligible_for_free_meal || !hasLoyaltyItem || hasNonLoyaltyItem;
if (loyaltyMessage) {
if (currentCustomer.eligible_for_free_meal) {
const count = currentCustomer.eligible_count || 1;
loyaltyMessage.innerHTML = `<span class="text-success fw-bold">Eligible for ${count} Free Product${count > 1 ? 's' : ''}!</span>`;
if (hasNonLoyaltyItem) {
loyaltyMessage.innerHTML += '<div class="text-danger small" style="font-size: 0.7rem;">(Remove non-loyalty products to redeem)</div>';
} else if (!hasLoyaltyItem) {
loyaltyMessage.innerHTML += '<div class="text-warning small" style="font-size: 0.7rem;">(Add a loyalty product to redeem)</div>';
}
} else {
loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`;
}
}
}
} else {
loyaltySection.classList.add('d-none');
}
}
function selectCustomer(cust) { function selectCustomer(cust) {
currentCustomer = cust; currentCustomer = cust;
if (selectedCustomerId) selectedCustomerId.value = cust.id; if (selectedCustomerId) selectedCustomerId.value = cust.id;
@ -262,18 +295,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (customerResults) customerResults.style.display = 'none'; if (customerResults) customerResults.style.display = 'none';
if (clearCustomerBtn) clearCustomerBtn.classList.remove('d-none'); if (clearCustomerBtn) clearCustomerBtn.classList.remove('d-none');
if (loyaltySection && typeof LOYALTY_SETTINGS !== 'undefined' && LOYALTY_SETTINGS.is_enabled) { updateLoyaltyUI();
loyaltySection.classList.remove('d-none');
if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = cust.points + ' pts';
if (redeemLoyaltyBtn) {
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 = '<span class="text-success fw-bold">Eligible for Free Meal!</span>';
else loyaltyMessage.textContent = `${cust.points_needed || 0} pts away from a free meal.`;
}
}
}
isLoyaltyRedemption = false; isLoyaltyRedemption = false;
} }
@ -287,7 +309,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
clearCustomerBtn.classList.add('d-none'); clearCustomerBtn.classList.add('d-none');
if (customerInfo) customerInfo.classList.add('d-none'); if (customerInfo) customerInfo.classList.add('d-none');
if (loyaltySection) loyaltySection.classList.add('d-none'); updateLoyaltyUI();
isLoyaltyRedemption = false; isLoyaltyRedemption = false;
updateCart(); updateCart();
}); });
@ -539,8 +561,10 @@ document.addEventListener('DOMContentLoaded', () => {
function updateCart() { function updateCart() {
if (!cartItemsContainer) return; if (!cartItemsContainer) return;
updateLoyaltyUI();
if (cart.length === 0) { if (cart.length === 0) {
if (redeemLoyaltyBtn) redeemLoyaltyBtn.disabled = true;
cartItemsContainer.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-basket3 fs-1 text-light"></i><p class="mt-2">${_t('cart_empty')}</p></div>`; cartItemsContainer.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-basket3 fs-1 text-light"></i><p class="mt-2">${_t('cart_empty')}</p></div>`;
if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0); if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0);
if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0); if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0);

View File

@ -0,0 +1,33 @@
function updateLoyaltyUI() {
if (!loyaltySection || typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return;
if (currentCustomer) {
loyaltySection.classList.remove('d-none');
if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = currentCustomer.points + ' pts';
if (redeemLoyaltyBtn) {
const hasLoyaltyItem = cart.some(item => item.is_loyalty);
const hasNonLoyaltyItem = cart.some(item => !item.is_loyalty);
redeemLoyaltyBtn.disabled = !currentCustomer.eligible_for_free_meal || !hasLoyaltyItem || hasNonLoyaltyItem;
if (loyaltyMessage) {
if (currentCustomer.eligible_for_free_meal) {
const count = currentCustomer.eligible_count || 1;
loyaltyMessage.innerHTML = `<span class="text-success fw-bold">Eligible for ${count} Free Product${count > 1 ? 's' : ''}!</span>`;
if (hasNonLoyaltyItem) {
loyaltyMessage.innerHTML += '<div class="text-danger small" style="font-size: 0.7rem;">(Remove non-loyalty products to redeem)</div>';
} else if (!hasLoyaltyItem) {
loyaltyMessage.innerHTML += '<div class="text-warning small" style="font-size: 0.7rem;">(Add a loyalty product to redeem)</div>';
}
} else {
loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`;
}
}
}
} else {
loyaltySection.classList.add('d-none');
}
}