sale-calculator-strategy
Version:
Sales calculation engine
178 lines (151 loc) • 5.46 kB
JavaScript
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
}
};