import { describe, it, expect } from '@jest/globals';
import {
calculateNPV,
calculateIRR,
calculatePaybackPeriod,
generateCashFlows,
CashFlowConfig,
} from '../../../src/core/calculators/financial.js';
describe('Financial Calculators', () => {
describe('calculateNPV', () => {
it('should calculate NPV correctly for positive cash flows', () => {
const cashFlows = [100, 100, 100, 100, 100];
const initialInvestment = -300;
const discountRate = 0.1 / 12; // 10% annual as monthly
const npv = calculateNPV(cashFlows, initialInvestment, discountRate);
expect(npv).toBeGreaterThan(0);
expect(npv).toBeCloseTo(185.94, 2);
});
it('should calculate negative NPV for poor investments', () => {
const cashFlows = [50, 50, 50, 50, 50];
const initialInvestment = -500;
const discountRate = 0.15 / 12; // 15% annual as monthly
const npv = calculateNPV(cashFlows, initialInvestment, discountRate);
expect(npv).toBeLessThan(0);
});
it('should handle zero discount rate', () => {
const cashFlows = [100, 100, 100];
const initialInvestment = -200;
const discountRate = 0;
const npv = calculateNPV(cashFlows, initialInvestment, discountRate);
expect(npv).toBe(100); // Sum of cash flows minus investment
});
});
describe('calculateIRR', () => {
it('should calculate IRR correctly for simple cash flows', () => {
const cashFlows = [-1000, 300, 300, 300, 300, 300];
const irr = calculateIRR(cashFlows);
expect(irr).toBeGreaterThan(0);
expect(irr).toBeCloseTo(0.0153, 4); // ~1.53% monthly
});
it('should return null for cash flows with no IRR', () => {
const cashFlows = [-1000, -500, -500]; // All negative
const irr = calculateIRR(cashFlows);
expect(irr).toBeNull();
});
it('should handle break-even scenarios', () => {
const cashFlows = [-1000, 500, 500];
const irr = calculateIRR(cashFlows);
expect(irr).toBeCloseTo(0, 4);
});
});
describe('calculatePaybackPeriod', () => {
it('should calculate payback period with linear interpolation', () => {
const cumulativeCashFlows = [-1000, -700, -350, 50, 500];
const payback = calculatePaybackPeriod(cumulativeCashFlows);
expect(payback).toBeCloseTo(2.875, 3); // Between month 2 and 3
});
it('should return null if investment never pays back', () => {
const cumulativeCashFlows = [-1000, -900, -850, -800, -750];
const payback = calculatePaybackPeriod(cumulativeCashFlows);
expect(payback).toBeNull();
});
it('should handle immediate payback', () => {
const cumulativeCashFlows = [100, 200, 300];
const payback = calculatePaybackPeriod(cumulativeCashFlows);
expect(payback).toBe(0);
});
});
describe('generateCashFlows', () => {
const baseConfig: CashFlowConfig = {
monthlyBenefit: 10000,
totalInvestment: 50000,
timelineMonths: 24,
implementationMonths: 3,
rampUpMonths: 3,
ongoingMonthlyCosts: 2000,
};
it('should generate correct cash flow pattern', () => {
const cashFlows = generateCashFlows(baseConfig);
expect(cashFlows).toHaveLength(24);
// Implementation period (months 0-2): negative
expect(cashFlows[0]).toBeLessThan(0);
expect(cashFlows[1]).toBeLessThan(0);
expect(cashFlows[2]).toBeLessThan(0);
// Ramp-up period (months 3-5): increasing
expect(cashFlows[3]).toBeLessThan(cashFlows[4]);
expect(cashFlows[4]).toBeLessThan(cashFlows[5]);
// Full benefit period (month 6+): stable
expect(cashFlows[6]).toBe(8000); // 10000 - 2000
expect(cashFlows[23]).toBe(8000);
});
it('should distribute investment across implementation months', () => {
const cashFlows = generateCashFlows(baseConfig);
const implementationCashFlows = cashFlows.slice(0, 3);
const totalImplementationCost = implementationCashFlows.reduce((sum, cf) => sum + cf, 0);
// Should include investment spread + ongoing costs
expect(totalImplementationCost).toBeCloseTo(-56000, 0); // -50000 - (3 * 2000)
});
it('should apply ramp-up correctly', () => {
const cashFlows = generateCashFlows(baseConfig);
// Month 3: 25% of benefit
expect(cashFlows[3]).toBeCloseTo(10000 * 0.25 - 2000, 0);
// Month 4: 50% of benefit
expect(cashFlows[4]).toBeCloseTo(10000 * 0.5 - 2000, 0);
// Month 5: 75% of benefit
expect(cashFlows[5]).toBeCloseTo(10000 * 0.75 - 2000, 0);
});
it('should handle zero ramp-up period', () => {
const config = { ...baseConfig, rampUpMonths: 0 };
const cashFlows = generateCashFlows(config);
// Should jump straight to full benefit after implementation
expect(cashFlows[3]).toBe(8000);
});
});
});