omnipay-savings-sdk
Version:
Omnipay Savings SDK
212 lines (211 loc) • 9.26 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateRequiredPeriodicAmountSimple = exports.calculateRequiredPeriodicAmountWithInterest = exports.calculateProjectedInterestEarnings = exports.calculateFinalBalanceWithCompoundInterest = void 0;
const constants_1 = require("./constants");
/**
* Get the number of days for each frequency type
*/
const getFrequencyDays = (frequency) => {
const frequencyEnum = (0, constants_1.getFrequencyEnum)(frequency);
switch (frequencyEnum) {
case 1:
return 1; // Daily
case 2:
return 7; // Weekly
case 3:
return 30; // Monthly
case 4:
return 1; // Save as you collect (treat as daily for calculation)
default:
return 1;
}
};
/**
* Calculate the total savings period in days
*/
const getSavingsPeriodDays = (startDate, endDate) => {
const timeDiff = endDate.getTime() - startDate.getTime();
return Math.ceil(timeDiff / (1000 * 3600 * 24));
};
/**
* Calculate final balance with compound interest
* Interest accrues daily but compounds monthly (on the 1st of each month)
*/
const calculateFinalBalanceWithCompoundInterest = (params) => {
const { periodicAmount, selectedFrequency, startDate, endDate, annualInterestRate, isInterestDisabled = false, currentBalance = 0, } = params;
if (!periodicAmount || !selectedFrequency || !startDate || !endDate)
return currentBalance;
const annualRate = annualInterestRate / 100;
const dailyRate = annualRate / 365;
const frequencyDays = getFrequencyDays(selectedFrequency);
let balance = currentBalance;
let accruedInterest = 0;
const currentDate = new Date(startDate);
const finalDate = new Date(endDate);
// Track when to add periodic deposits
let nextDepositDate = new Date(startDate);
while (currentDate <= finalDate) {
// Add periodic deposit if it's time
if (currentDate >= nextDepositDate) {
balance += periodicAmount;
// Calculate next deposit date
nextDepositDate = new Date(nextDepositDate);
nextDepositDate.setDate(nextDepositDate.getDate() + frequencyDays);
}
// Accrue daily interest on current balance (only if interest is enabled)
if (balance > 0 && !isInterestDisabled) {
accruedInterest += balance * dailyRate;
}
// Compound interest monthly (add to balance on 1st of each month)
if (currentDate.getDate() === 1 && accruedInterest > 0) {
balance += accruedInterest;
accruedInterest = 0;
}
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
// Add any remaining accrued interest at the end
balance += accruedInterest;
return balance;
};
exports.calculateFinalBalanceWithCompoundInterest = calculateFinalBalanceWithCompoundInterest;
/**
* Calculate total interest earned over the savings period
*/
const calculateProjectedInterestEarnings = (params) => {
const { periodicAmount, selectedFrequency, startDate, endDate, annualInterestRate, isInterestDisabled = false, currentBalance = 0, } = params;
if (!periodicAmount ||
!annualInterestRate ||
isInterestDisabled ||
!startDate ||
!endDate)
return 0;
const annualRate = annualInterestRate / 100;
const dailyRate = annualRate / 365;
const frequencyDays = getFrequencyDays(selectedFrequency);
let balance = currentBalance;
let accruedInterest = 0;
let totalInterestEarned = 0;
const currentDate = new Date(startDate);
const finalDate = new Date(endDate);
// Track when to add periodic deposits
let nextDepositDate = new Date(startDate);
while (currentDate <= finalDate) {
// Add periodic deposit if it's time
if (currentDate >= nextDepositDate) {
balance += periodicAmount;
// Calculate next deposit date
nextDepositDate = new Date(nextDepositDate);
nextDepositDate.setDate(nextDepositDate.getDate() + frequencyDays);
}
// Accrue daily interest on current balance
if (balance > 0) {
accruedInterest += balance * dailyRate;
}
// Compound interest monthly (add to balance on 1st of each month)
if (currentDate.getDate() === 1 && accruedInterest > 0) {
balance += accruedInterest;
totalInterestEarned += accruedInterest;
accruedInterest = 0;
}
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
// Add any remaining accrued interest at the end
totalInterestEarned += accruedInterest;
return totalInterestEarned;
};
exports.calculateProjectedInterestEarnings = calculateProjectedInterestEarnings;
/**
* Calculate the required periodic amount to reach a savings target, considering compound interest
* Uses binary search to find the optimal periodic amount
*/
const calculateRequiredPeriodicAmountWithInterest = (params) => {
const { savingsTarget, selectedFrequency, startDate, endDate, annualInterestRate, isInterestDisabled = false, currentBalance = 0, } = params;
if (!savingsTarget || !selectedFrequency || !startDate || !endDate)
return 0;
const periodDays = getSavingsPeriodDays(startDate, endDate);
const frequencyDays = getFrequencyDays(selectedFrequency);
const numberOfPeriods = Math.floor(periodDays / frequencyDays);
if (numberOfPeriods <= 0)
return 0;
// If interest is disabled, use simple calculation
if (isInterestDisabled || annualInterestRate === 0) {
const remainingTarget = Math.max(0, savingsTarget - currentBalance);
return Math.ceil(remainingTarget / numberOfPeriods);
}
// Account for current balance - we need less savings target
const remainingTarget = Math.max(0, savingsTarget - currentBalance);
if (remainingTarget <= 0)
return 0;
// Binary search to find the optimal periodic amount
let low = 0;
let high = remainingTarget; // Start with a reasonable upper bound
let bestAmount = 0;
const tolerance = 1; // Allow 1 naira tolerance
// If interest will likely contribute significantly, reduce the upper bound
if (annualInterestRate > 0) {
// Rough estimate: reduce upper bound by potential interest contribution
const estimatedInterestContribution = (remainingTarget * (annualInterestRate / 100) * (periodDays / 365)) / 2;
high = Math.max(100, remainingTarget - estimatedInterestContribution);
}
let iterations = 0;
const maxIterations = 50; // Prevent infinite loops
while (low <= high && iterations < maxIterations) {
iterations++;
const mid = Math.floor((low + high) / 2);
// Calculate what the final balance would be with this periodic amount
const finalBalance = (0, exports.calculateFinalBalanceWithCompoundInterest)({
periodicAmount: mid,
selectedFrequency,
startDate,
endDate,
annualInterestRate,
isInterestDisabled,
currentBalance,
});
const difference = finalBalance - savingsTarget;
// If we're within tolerance, we found a good amount
if (Math.abs(difference) <= tolerance) {
bestAmount = mid;
break;
}
if (finalBalance < savingsTarget) {
// Need to save more
low = mid + 1;
bestAmount = mid + 1; // Keep track of the last insufficient amount
}
else {
// We're saving too much, can reduce
high = mid - 1;
bestAmount = mid; // This amount reaches the target
}
}
// Ensure we don't return 0 if we couldn't find exact match
if (bestAmount === 0 && remainingTarget > 0) {
// Fallback to simple calculation with a small buffer for interest
const simpleAmount = Math.ceil(remainingTarget / numberOfPeriods);
const interestBuffer = isInterestDisabled
? 0
: Math.max(1, simpleAmount * 0.05); // 5% buffer
bestAmount = Math.max(1, simpleAmount - interestBuffer);
}
return Math.max(1, Math.ceil(bestAmount)); // Always return at least 1 naira
};
exports.calculateRequiredPeriodicAmountWithInterest = calculateRequiredPeriodicAmountWithInterest;
/**
* Simple calculation without interest (for backward compatibility)
*/
const calculateRequiredPeriodicAmountSimple = (params) => {
const { savingsTarget, selectedFrequency, startDate, endDate, currentBalance = 0, } = params;
if (!savingsTarget || !selectedFrequency || !startDate || !endDate)
return 0;
const periodDays = getSavingsPeriodDays(startDate, endDate);
const frequencyDays = getFrequencyDays(selectedFrequency);
const numberOfPeriods = Math.floor(periodDays / frequencyDays);
if (numberOfPeriods <= 0)
return 0;
const remainingTarget = Math.max(0, savingsTarget - currentBalance);
return Math.ceil(remainingTarget / numberOfPeriods);
};
exports.calculateRequiredPeriodicAmountSimple = calculateRequiredPeriodicAmountSimple;