UNPKG

sale-calculator-strategy

Version:

Sales calculation engine

178 lines (151 loc) 5.46 kB
let products = {}; let productTaxes = {}; let taxes = {}; let wholesaleLevels = {}; let settings = {}; /** * Sales Calculation Engine (V1) * @author softseti * * Core logic for calculating sale totals with support for: * - Wholesale pricing levels * - Product-specific and general taxes * - Deal/discount allocations */ // ============================================== // INITIALIZATION // ============================================== /** * Initializes the calculator with indexed data * @param {Object} params - Data collections: * - products: {id: Product} * - product_taxes: {product_id: TaxAssociation[]} * - taxes: {id: Tax} * - wholesale_levels: {product_id: WholesaleLevel[]} * - settings: GlobalSettings */ function init(params) { products = params.products || {}; productTaxes = params.product_taxes || {}; taxes = params.taxes || {}; wholesaleLevels = params.wholesale_levels || {}; settings = params.settings || {}; } // ============================================== // CORE CALCULATION ENGINE (V2 SCOPE) // ============================================== function calcTotal(sale) { validateSaleStructure(sale); sale = preprocessSale(sale); return sale.dwSaleProducts.reduce((total, product) => { const discount = calcDwSaleProductDiscount(product, sale); const productSum = calcDwSaleProductSum(product, sale) - discount; const taxes = getApplicableProductTaxes(product, productSum, sale); return total + productSum + taxes; }, 0); } function preprocessSale(sale) { if (sale.dwSaleDeals) { sale.dwSaleDeals = sale.dwSaleDeals.map(deal => ({ ...deal, sum: deal.amount * -1 })); } return sale; } function calcDwSaleProductSum(saleProduct, sale) { const product = products[saleProduct.product_id]; if (!product) { throw new Error(`Product ${saleProduct.product_id} not found`); } const price = saleProduct.custom_price ?? calcDwSaleProductPrice(saleProduct, sale); return (saleProduct.quantity * price) / product.quantity; } function calcDwSaleProductPrice(saleProduct, sale) { const product = products[saleProduct.product_id]; const wholeSaleLevel = getProductWholesaleLevel(saleProduct, sale); return wholeSaleLevel?.price ?? product.original_public_price; } function getProductWholesaleLevel(saleProduct, sale) { const product = products[saleProduct.product_id]; const quantity = settings.wholesale_level_quantity_restriction === 'sale' ? sumProductQuantity(sale.dwSaleProducts, product.id) : saleProduct.quantity; const levels = wholesaleLevels[product.id] || []; return levels.find(l => quantity >= l.quantity_min && quantity <= l.quantity_max ) || null; } function calcDwSaleProductDiscount(product, sale) { const discount = sale.dwSaleDeals.reduce((total, deal) => total + Math.abs(deal.sum), 0); const subtotal = calcSubtotal(sale) + discount; const productSum = calcDwSaleProductSum(product, sale); const productPercent = (productSum * 100) / subtotal; return (productPercent / 100) * discount; } function calcSubtotal(sale) { const productSum = sale.dwSaleProducts.reduce( (sum, saleProduct) => sum + calcDwSaleProductSum(saleProduct, sale), 0); const dealsSum = sale.dwSaleDeals.reduce((sum, d) => sum + d.sum, 0); return productSum + dealsSum; } function getApplicableProductTaxes(saleProduct, productSum, sale) { const specificTaxes = getProductSpecificTaxes(saleProduct, sale); const generalTaxes = getGeneralTaxes(sale); return [...specificTaxes, ...generalTaxes].reduce( (total, tax) => total + (productSum * (tax.rate / 100)), 0 ); } function getProductSpecificTaxes(saleProduct, sale) { const taxReceiptTypeKey = getTaxReceiptTypeKey(sale.receipt_type_key); const associations = productTaxes[saleProduct.product_id] || []; const taxMap = {}; associations.forEach(({ tax_id }) => { const tax = taxes[tax_id]; if (tax && !tax.general && tax[taxReceiptTypeKey]) { taxMap[getTaxIndex(tax)] = tax; } }); return Object.values(taxMap); } function getGeneralTaxes(sale) { const taxReceiptTypeKey = getTaxReceiptTypeKey(sale.receipt_type_key); return Object.values(taxes).filter(t => t.active && t.general && t[taxReceiptTypeKey] ); } // ============================================== // HELPERS // ============================================== function validateSaleStructure(sale) { const required = ['receipt_type_key', 'company_id', 'branch_id', 'dwSaleProducts']; if (required.some(field => !sale[field])) { throw new Error("Estructura de venta inválida"); } if (Object.keys(products).length === 0) { throw new Error("Productos no cargados"); } } function sumProductQuantity(products, productId) { return products.filter(p => p.product_id === productId) .reduce((sum, p) => sum + p.quantity, 0); } function getTaxIndex(tax) { return `${tax.abbreviation.toLowerCase()}${tax.rate.toFixed(6).replace('.', '')}`; } function getTaxReceiptTypeKey(receiptType) { return receiptType === 'note' ? 'sales_note' : receiptType; } module.exports = { init, calcTotal, _internals: { calcDwSaleProductSum, calcDwSaleProductDiscount, getApplicableProductTaxes } };