softseti-sale-calculator-library
Version:
Sales calculation engine by Softseti
180 lines (154 loc) • 6.31 kB
JavaScript
// src/refund-calculator.js
const {
validateRefund,
calculatePreviousRefundsTotal,
calculateMaxRefundableAmount,
findProductInSale
} = require('./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;