UNPKG

omnipay-savings-sdk

Version:

Omnipay Savings SDK

212 lines (211 loc) 9.26 kB
"use strict"; 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;