266 lines
9.9 KiB
JavaScript
266 lines
9.9 KiB
JavaScript
/**
|
|
* BOGO Service - Handles Buy One Get One logic
|
|
*
|
|
* Processes BOGO rules from the API and calculates free items/discounts
|
|
*/
|
|
const BogoService = {
|
|
/**
|
|
* Check if a product has any BOGO rules
|
|
* @param {Object} product
|
|
* @returns {boolean}
|
|
*/
|
|
hasBogoRules(product) {
|
|
return product.bogo_rules && product.bogo_rules.length > 0;
|
|
},
|
|
|
|
/**
|
|
* Get the primary BOGO rule for a product (first one)
|
|
* @param {Object} product
|
|
* @returns {Object|null}
|
|
*/
|
|
getPrimaryBogoRule(product) {
|
|
if (!this.hasBogoRules(product)) return null;
|
|
return product.bogo_rules[0];
|
|
},
|
|
|
|
/**
|
|
* Get BOGO label for display
|
|
* @param {Object} product
|
|
* @returns {string|null}
|
|
*/
|
|
getBogoLabel(product) {
|
|
const rule = this.getPrimaryBogoRule(product);
|
|
return rule ? rule.label : null;
|
|
},
|
|
|
|
/**
|
|
* Calculate BOGO discount for cart items
|
|
*
|
|
* @param {Array} cartItems - Array of cart items with product data
|
|
* @param {Array} products - Full products list with BOGO rules
|
|
* @returns {Object} { freeItems: [], discountAmount: number }
|
|
*/
|
|
calculateBogoForCart(cartItems, products) {
|
|
const result = {
|
|
freeItems: [],
|
|
discountAmount: 0,
|
|
appliedRules: []
|
|
};
|
|
|
|
// Group cart items by product ID
|
|
const itemsByProduct = this.groupCartItemsByProduct(cartItems);
|
|
|
|
// Check each product group for BOGO eligibility
|
|
for (const [productId, items] of Object.entries(itemsByProduct)) {
|
|
const product = products.find(p => p.id == productId);
|
|
if (!product || !this.hasBogoRules(product)) continue;
|
|
|
|
const rule = this.getPrimaryBogoRule(product);
|
|
const quantity = items.length;
|
|
|
|
// Calculate how many free items based on the rule
|
|
const bogoResult = this.applyBogoRule(rule, quantity, product, products);
|
|
|
|
if (bogoResult.freeItems.length > 0 || bogoResult.discountAmount > 0) {
|
|
result.freeItems.push(...bogoResult.freeItems);
|
|
result.discountAmount += bogoResult.discountAmount;
|
|
result.appliedRules.push({
|
|
rule,
|
|
productId: parseInt(productId),
|
|
productName: product.name,
|
|
...bogoResult
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Group cart items by product ID
|
|
* @param {Array} cartItems
|
|
* @returns {Object}
|
|
*/
|
|
groupCartItemsByProduct(cartItems) {
|
|
return cartItems.reduce((groups, item) => {
|
|
const id = item.id;
|
|
if (!groups[id]) groups[id] = [];
|
|
groups[id].push(item);
|
|
return groups;
|
|
}, {});
|
|
},
|
|
|
|
/**
|
|
* Apply a BOGO rule to calculate free items/discount
|
|
*
|
|
* @param {Object} rule - BOGO rule from API
|
|
* @param {number} quantity - Number of items in cart
|
|
* @param {Object} product - The product
|
|
* @param {Array} products - All products (for Buy X Get Y)
|
|
* @returns {Object}
|
|
*/
|
|
applyBogoRule(rule, quantity, product, products) {
|
|
const result = {
|
|
freeItems: [],
|
|
discountAmount: 0,
|
|
freeQuantity: 0
|
|
};
|
|
|
|
if (quantity < rule.buy_qty) {
|
|
return result; // Not enough items to trigger BOGO
|
|
}
|
|
|
|
// Calculate how many times the rule applies
|
|
let timesApplied = 1;
|
|
if (rule.recursive) {
|
|
timesApplied = Math.floor(quantity / rule.buy_qty);
|
|
}
|
|
|
|
const freeQty = timesApplied * rule.get_qty;
|
|
result.freeQuantity = freeQty;
|
|
|
|
if (rule.type === 'buy_x_get_x') {
|
|
// Same product is free or discounted
|
|
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
|
|
// Fully free - automatically add get_qty items to cart
|
|
result.discountAmount = freeQty * parseFloat(product.price);
|
|
|
|
// Add get_qty free items (not just 1)
|
|
for (let i = 0; i < freeQty; i++) {
|
|
result.freeItems.push({
|
|
id: product.id,
|
|
name: product.name + ' (GRATIS)',
|
|
price: '0.00',
|
|
originalPrice: product.price,
|
|
isFree: true,
|
|
ruleId: rule.rule_id
|
|
});
|
|
}
|
|
}
|
|
// For percentage and flat discounts, we DON'T automatically add items
|
|
// These are handled manually via addBogoDiscountedItem() in cart.js
|
|
// The user clicks a button to add the discounted item
|
|
} else if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
|
|
// Different product (Y) is free or discounted
|
|
console.log('[BOGO DEBUG] Processing buy_x_get_y rule:', {
|
|
rule_id: rule.rule_id,
|
|
rule_title: rule.rule_title,
|
|
get_product_ids: rule.get_product_ids,
|
|
discount_type: rule.discount_type,
|
|
discount_value: rule.discount_value,
|
|
freeQty: freeQty
|
|
});
|
|
|
|
// For buy_x_get_y with free_product, we AUTO-ADD the free items
|
|
// For other discount types, user must manually add them
|
|
for (const freeProductId of rule.get_product_ids) {
|
|
const freeProduct = products.find(p => p.id == freeProductId);
|
|
if (!freeProduct) {
|
|
console.log('[BOGO DEBUG] Free product not found:', freeProductId);
|
|
continue;
|
|
}
|
|
|
|
const freeProductPrice = parseFloat(freeProduct.price);
|
|
console.log('[BOGO DEBUG] Found free product:', {
|
|
id: freeProduct.id,
|
|
name: freeProduct.name,
|
|
price: freeProductPrice
|
|
});
|
|
|
|
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
|
|
// 100% korting - product Y is gratis - AUTO-ADD to cart
|
|
result.discountAmount += freeQty * freeProductPrice;
|
|
|
|
console.log('[BOGO DEBUG] Adding free items to cart:', freeQty);
|
|
// Add get_qty free items
|
|
for (let i = 0; i < freeQty; i++) {
|
|
result.freeItems.push({
|
|
id: freeProduct.id,
|
|
name: freeProduct.name + ' (GRATIS)',
|
|
price: '0.00',
|
|
originalPrice: freeProduct.price,
|
|
isFree: true,
|
|
ruleId: rule.rule_id
|
|
});
|
|
}
|
|
} else if (rule.discount_type === 'percentage') {
|
|
// Percentage korting op product Y (but not auto-added)
|
|
const discountPerItem = freeProductPrice * (rule.discount_value / 100);
|
|
result.discountAmount += freeQty * discountPerItem;
|
|
console.log('[BOGO DEBUG] Percentage discount (not auto-added):', rule.discount_value + '%');
|
|
} else if (rule.discount_type === 'flat') {
|
|
// Vaste korting op product Y (but not auto-added)
|
|
const discountedPrice = Math.max(0, freeProductPrice - rule.discount_value).toFixed(2);
|
|
const actualDiscount = freeProductPrice - parseFloat(discountedPrice);
|
|
result.discountAmount += freeQty * actualDiscount;
|
|
console.log('[BOGO DEBUG] Flat discount (not auto-added):', rule.discount_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Get suggested free items for a product (for display before adding to cart)
|
|
*
|
|
* @param {Object} product
|
|
* @param {Array} products
|
|
* @returns {Array}
|
|
*/
|
|
getSuggestedFreeItems(product, products) {
|
|
if (!this.hasBogoRules(product)) return [];
|
|
|
|
const rule = this.getPrimaryBogoRule(product);
|
|
const suggestions = [];
|
|
|
|
if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
|
|
for (const freeProductId of rule.get_product_ids) {
|
|
const freeProduct = products.find(p => p.id == freeProductId);
|
|
if (freeProduct) {
|
|
suggestions.push({
|
|
...freeProduct,
|
|
bogoLabel: `Gratis bij ${rule.buy_qty}x ${product.name}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return suggestions;
|
|
},
|
|
|
|
/**
|
|
* Get BOGO discount info for a Y product based on X product's rules
|
|
* Checks ALL BOGO rules on the X product, not just the primary one
|
|
*
|
|
* @param {Object} xProduct - The main product (X) with BOGO rules
|
|
* @param {Object} yProduct - The upsell product (Y) to check
|
|
* @returns {Object|null} - Discount info or null if no discount applies
|
|
*/
|
|
getYProductDiscount(xProduct, yProduct) {
|
|
if (!xProduct || !this.hasBogoRules(xProduct)) return null;
|
|
|
|
// Check ALL BOGO rules, not just the primary one
|
|
for (const rule of xProduct.bogo_rules) {
|
|
// Only for buy_x_get_y rules
|
|
if (rule.type !== 'buy_x_get_y') continue;
|
|
|
|
// Check if yProduct is in the get_product_ids
|
|
if (!rule.get_product_ids || !rule.get_product_ids.includes(parseInt(yProduct.id))) {
|
|
continue;
|
|
}
|
|
|
|
// Return discount info for the first matching rule
|
|
return {
|
|
rule_id: rule.rule_id,
|
|
discount_type: rule.discount_type,
|
|
discount_value: rule.discount_value,
|
|
label: rule.label,
|
|
badge_text: rule.badge_text
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|