UNPKG

softseti-sale-calculator-library

Version:

Sales calculation engine by Softseti

180 lines (154 loc) 6.32 kB
// src/refund-calculator.js const { validateRefund, calculatePreviousRefundsTotal, calculateMaxRefundableAmount, findProductInSale } = require('../utils/helpers'); class RefundCalculator { static calculateRefund(sale, returnItems, options, currentMemoId) { const existingCreditMemo = options.credit_memo || null; // Validate the refund request const validationError = validateRefund(sale, returnItems, options, currentMemoId); if (validationError) { const errorResponse = { valid: false, message: validationError, total_refund: existingCreditMemo?.total_refund || 0, return_items: existingCreditMemo?.return_items || [], remaining_sale_total: sale.total - calculatePreviousRefundsTotal(sale, currentMemoId), previous_refunds_total: calculatePreviousRefundsTotal(sale, currentMemoId), input_type: options.input_type }; // Merge additional fields from existing credit memo if (existingCreditMemo) { Object.keys(existingCreditMemo).forEach(key => { if (!errorResponse.hasOwnProperty(key)) { errorResponse[key] = existingCreditMemo[key]; } }); // Ensure validity reflects current error state errorResponse.valid = false; } return errorResponse; } const previousRefundsTotal = calculatePreviousRefundsTotal(sale, options, currentMemoId); const maxRefundable = calculateMaxRefundableAmount(sale, currentMemoId); let totalRefund = 0; let calculatedItems = []; // Use existing items if available, otherwise calculate new ones if (existingCreditMemo?.return_items) { totalRefund = existingCreditMemo.return_items.reduce((sum, item) => sum + (item.refund_amount || 0), 0); } // Calculate refund based on input type switch (options.input_type) { case 'amount_only': calculatedItems = RefundCalculator.calculateAmountOnlyRefund( sale, options.requested_refund_amount || 0, maxRefundable, options ); break; case 'items_with_custom_amount': calculatedItems = RefundCalculator.calculateCustomAmountRefund( sale, returnItems, options ); break; case 'items_full_refund': default: calculatedItems = RefundCalculator.calculateFullItemRefund( sale, returnItems, options ); } totalRefund = calculatedItems.reduce((sum, item) => sum + (item.refund_amount || 0), 0); // Scale down if exceeds max refundable amount (only for new calculations) if (!existingCreditMemo && totalRefund > maxRefundable) { const scaleFactor = maxRefundable / totalRefund; calculatedItems.forEach(item => { item.refund_amount = parseFloat((item.refund_amount * scaleFactor).toFixed(options.decimal_precision)); }); totalRefund = calculatedItems.reduce((sum, item) => sum + item.refund_amount, 0); } let remaining = 0 if (existingCreditMemo?.remaining_sale_total) { remaining = sale.total - previousRefundsTotal - totalRefund; } else { remaining = sale.total - previousRefundsTotal - totalRefund; } // Build result with calculated values const result = { ...sale, valid: true, total_refund: parseFloat(totalRefund.toFixed(options.decimal_precision)), return_items: calculatedItems, remaining_sale_total: parseFloat(remaining.toFixed(options.decimal_precision)), previous_refunds_total: previousRefundsTotal, input_type: options.input_type }; // Preserve all existing credit memo fields not in our result if (existingCreditMemo) { Object.keys(existingCreditMemo).forEach(key => { if (!result.hasOwnProperty(key) && key !== 'return_items') { result[key] = existingCreditMemo[key]; } }); } return result; } static calculateAmountOnlyRefund(sale, requestedAmount, maxRefundable, options) { const refundAmount = Math.min(requestedAmount, maxRefundable); const totalSaleValue = sale.return_items.reduce( (sum, item) => sum + (item.price * item.sale_product_quantity), 0 ); return sale.return_items.map(saleItem => { const itemValue = saleItem.price * saleItem.sale_product_quantity; const proportion = totalSaleValue > 0 ? itemValue / totalSaleValue : 0; const itemRefund = parseFloat((refundAmount * proportion).toFixed(options.decimal_precision)); const proportionalQuantity = saleItem.sale_product_quantity * proportion; // Preserve all existing item properties return { ...saleItem, // Keep all existing properties quantity: parseFloat(proportionalQuantity.toFixed(options.decimal_precision)), refund_amount: itemRefund }; }); } static calculateCustomAmountRefund(sale, returnItems, options) { return returnItems.map(returnItem => { const saleItem = findProductInSale(sale, returnItem.product_id); if (!saleItem) return null; const refundAmount = returnItem.refund_amount || 0; const proportionalQuantity = saleItem.price !== 0 ? parseFloat((refundAmount / saleItem.price).toFixed(options.decimal_precision)) : 0; // Preserve all existing item properties return { ...saleItem, // Keep all existing properties ...returnItem, // Merge with input properties quantity: proportionalQuantity, refund_amount: refundAmount }; }).filter(item => item !== null); } static calculateFullItemRefund(sale, returnItems, options) { return returnItems.map(returnItem => { const saleItem = findProductInSale(sale, returnItem.product_id); if (!saleItem) return null; const quantity = returnItem.quantity ?? 0; const refundAmount = saleItem.price * quantity; // Preserve all existing item properties return { ...saleItem, // Keep all existing properties ...returnItem, // Merge with input properties quantity: quantity, refund_amount: parseFloat(refundAmount.toFixed(options.decimal_precision)) }; }).filter(item => item !== null); } } module.exports = RefundCalculator;