/** * 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 * * @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; const rule = this.getPrimaryBogoRule(xProduct); // Only for buy_x_get_y rules if (rule.type !== 'buy_x_get_y') return null; // Check if yProduct is in the get_product_ids if (!rule.get_product_ids || !rule.get_product_ids.includes(parseInt(yProduct.id))) { return null; } // Return discount info return { rule_id: rule.rule_id, discount_type: rule.discount_type, discount_value: rule.discount_value, label: rule.label, badge_text: rule.badge_text }; } };