import {
calculateNPV,
calculateNPVDerivative,
calculatePaybackPeriod,
generateCashFlows as generateCashFlowsUtil,
calculateBreakEvenDate as calculateBreakEvenDateUtil,
CashFlowConfig,
FINANCIAL_CONSTANTS
} from './financial-utils.js';
import { logger, createLogger } from '../../utils/logger.js';
import { CalculationError, RangeError } from '../../utils/errors.js';
import { validateFinancialAmount, validateMonths, validateRate } from '../../utils/validators.js';
export class FinancialCalculator {
private logger = createLogger({ component: 'FinancialCalculator' });
/**
* Calculate Net Present Value (NPV)
* @param cashFlows Array of cash flows (negative for costs, positive for benefits)
* @param discountRate Monthly discount rate (e.g., 0.1/12 for 10% annual)
*/
calculateNPV(cashFlows: number[], discountRate: number): number {
try {
this.logger.debug('Calculating NPV', {
cashFlowCount: cashFlows.length,
discountRate
});
// Validate inputs
if (!Array.isArray(cashFlows) || cashFlows.length === 0) {
throw new CalculationError('Cash flows must be a non-empty array');
}
validateRate(discountRate, 'discountRate');
// Validate each cash flow
cashFlows.forEach((flow, index) => {
if (!isFinite(flow)) {
throw new CalculationError(`Invalid cash flow at index ${index}`, {
index,
value: flow
});
}
});
const npv = calculateNPV(cashFlows, discountRate);
if (!isFinite(npv)) {
throw new CalculationError('NPV calculation resulted in non-finite value', {
cashFlowsLength: cashFlows.length,
discountRate
});
}
return npv;
} catch (error) {
this.logger.error('NPV calculation failed', error as Error);
throw error;
}
}
/**
* Calculate Internal Rate of Return (IRR)
* Uses Newton's method for approximation
*/
calculateIRR(cashFlows: number[], initialGuess: number = 0.1): number {
try {
this.logger.debug('Calculating IRR', {
cashFlowCount: cashFlows.length,
initialGuess
});
// Validate inputs
if (!Array.isArray(cashFlows) || cashFlows.length === 0) {
throw new CalculationError('Cash flows must be a non-empty array');
}
// Check if all cash flows have the same sign (no IRR exists)
const hasPositive = cashFlows.some(cf => cf > 0);
const hasNegative = cashFlows.some(cf => cf < 0);
if (!hasPositive || !hasNegative) {
throw new CalculationError(
'IRR requires cash flows with different signs (both inflows and outflows)'
);
}
const maxIterations = 100;
const tolerance = 1e-6;
let rate = initialGuess;
let iterations = 0;
for (let i = 0; i < maxIterations; i++) {
iterations++;
try {
const npv = calculateNPV(cashFlows, rate);
const npvDerivative = calculateNPVDerivative(cashFlows, rate);
if (Math.abs(npvDerivative) < tolerance) {
this.logger.warn('IRR calculation: derivative too small', {
iteration: i,
derivative: npvDerivative
});
break;
}
let newRate = rate - npv / npvDerivative;
// Bound the rate to prevent extreme values
if (newRate < -0.99) {
newRate = -0.99;
} else if (newRate > 10) {
newRate = 10;
}
if (Math.abs(newRate - rate) < tolerance) {
this.logger.debug('IRR converged', {
iterations: i + 1,
finalRate: newRate
});
return newRate;
}
rate = newRate;
} catch (error) {
this.logger.error('Error in IRR iteration', error as Error, { iteration: i });
throw new CalculationError(`IRR calculation failed at iteration ${i}`, {
iteration: i,
rate,
error: (error as Error).message
});
}
}
// If we didn't converge, throw an error
throw new CalculationError('IRR calculation did not converge', {
iterations,
lastRate: rate,
tolerance
});
} catch (error) {
this.logger.error('IRR calculation failed', error as Error);
throw error;
}
}
/**
* Calculate payback period in months
*/
calculatePaybackPeriod(cashFlows: number[]): number {
return calculatePaybackPeriod(cashFlows);
}
/**
* Calculate 5-year ROI percentage
*/
calculate5YearROI(totalCosts: number, monthlyBenefits: number, ongoingMonthlyCosts: number = 0): number {
// Validate inputs
if (!isFinite(totalCosts) || totalCosts < 0) {
this.logger.warn('Invalid totalCosts for ROI calculation', { totalCosts });
return 0;
}
if (!isFinite(monthlyBenefits) || monthlyBenefits < 0) {
this.logger.warn('Invalid monthlyBenefits for ROI calculation', { monthlyBenefits });
return 0;
}
const totalBenefits = monthlyBenefits * FINANCIAL_CONSTANTS.FIVE_YEAR_MONTHS;
const totalOngoingCosts = ongoingMonthlyCosts * FINANCIAL_CONSTANTS.FIVE_YEAR_MONTHS;
const netBenefit = totalBenefits - totalCosts - totalOngoingCosts;
const totalInvestment = totalCosts + totalOngoingCosts;
// Log calculation details for debugging
this.logger.debug('5-Year ROI Calculation', {
totalCosts,
monthlyBenefits,
ongoingMonthlyCosts,
totalBenefits,
totalOngoingCosts,
netBenefit,
totalInvestment
});
// Avoid division by zero - if no investment, return 0 or infinity based on benefits
if (totalInvestment === 0) {
return netBenefit > 0 ? Infinity : 0;
}
const roi = (netBenefit / totalInvestment) * 100;
// Sanity check - ROI above 10,000% is likely an error
if (roi > 10000) {
this.logger.warn('Suspiciously high ROI calculated', {
roi,
totalCosts,
monthlyBenefits,
totalInvestment,
netBenefit
});
// Cap at a more reasonable maximum
return 10000;
}
return roi;
}
/**
* Generate monthly cash flows from costs and benefits
*/
generateCashFlows(
initialInvestment: number,
monthlyBenefits: number,
timelineMonths: number,
implementationMonths: number = FINANCIAL_CONSTANTS.DEFAULT_IMPLEMENTATION_MONTHS,
rampUpMonths: number = FINANCIAL_CONSTANTS.DEFAULT_RAMP_UP_MONTHS,
ongoingMonthlyCosts: number = 0
): number[] {
const config: CashFlowConfig = {
initialInvestment,
monthlyBenefits,
timelineMonths,
implementationMonths,
rampUpMonths,
ongoingMonthlyCosts
};
return generateCashFlowsUtil(config);
}
/**
* Calculate break-even date
*/
calculateBreakEvenDate(
startDate: Date,
paybackPeriodMonths: number
): Date {
return calculateBreakEvenDateUtil(startDate, paybackPeriodMonths);
}
}