231 lines
8.2 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 to cart
result.discountAmount = freeQty * parseFloat(product.price);
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
// For buy_x_get_y, we DON'T automatically add items to cart
// The user must manually add them via the upsell section
// We only track the discount amount for display purposes
for (const freeProductId of rule.get_product_ids) {
const freeProduct = products.find(p => p.id == freeProductId);
if (!freeProduct) continue;
const freeProductPrice = parseFloat(freeProduct.price);
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
// 100% korting - product Y is gratis (but not auto-added)
result.discountAmount += freeQty * freeProductPrice;
} 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;
} 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;
}
}
}
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;
}
};