import { FinancialCalculator } from './financial.js';
import { FINANCIAL_CONSTANTS, getAutomationFactor } from './financial-utils.js';
import { createLogger } from '../../utils/logger.js';
import type { UseCase } from '../../schemas/use-case.js';
import type {
ProjectionCreate,
FinancialMetrics,
ROICalculations
} from '../../schemas/projection.js';
export interface ROIEngineConfig {
defaultDiscountRate: number;
implementationMonths: number;
rampUpMonths: number;
confidenceMultipliers: {
conservative: number;
expected: number;
optimistic: number;
};
}
export class ROIEngine {
private calculator: FinancialCalculator;
private config: ROIEngineConfig;
constructor(config?: Partial<ROIEngineConfig>) {
this.calculator = new FinancialCalculator();
this.config = {
defaultDiscountRate: FINANCIAL_CONSTANTS.DEFAULT_DISCOUNT_RATE,
implementationMonths: FINANCIAL_CONSTANTS.DEFAULT_IMPLEMENTATION_MONTHS,
rampUpMonths: FINANCIAL_CONSTANTS.DEFAULT_RAMP_UP_MONTHS,
confidenceMultipliers: {
conservative: 0.7,
expected: 1.0,
optimistic: 1.3
},
...config
};
}
/**
* Calculate ROI projection from use cases
*
* Note on development hours:
* - If implementationCosts.development_hours > 0: Assumes this is the total for the entire project
* - If implementationCosts.development_hours = 0: Sums up individual use case development hours
* - This prevents double-counting when project-level hours already include use case work
*/
calculateProjection(
projectId: string,
useCases: UseCase[],
implementationCosts: ProjectionCreate['implementation_costs'],
timelineMonths: number,
scenarioName: string = 'Base Case'
): ProjectionCreate {
// Aggregate benefits from all use cases
const aggregatedMetrics = this.aggregateUseCaseMetrics(useCases);
// Calculate financial metrics for each confidence level
const financialMetrics = {
conservative: this.calculateFinancialMetrics(
aggregatedMetrics,
this.config.confidenceMultipliers.conservative
),
expected: this.calculateFinancialMetrics(
aggregatedMetrics,
this.config.confidenceMultipliers.expected
),
optimistic: this.calculateFinancialMetrics(
aggregatedMetrics,
this.config.confidenceMultipliers.optimistic
)
};
// Use expected case for main calculations
const totalInvestment = this.calculateTotalInvestment(implementationCosts, useCases);
const monthlyBenefit = financialMetrics.expected.total_monthly_benefit;
// Generate cash flows
const cashFlows = this.calculator.generateCashFlows(
totalInvestment,
monthlyBenefit,
timelineMonths,
this.config.implementationMonths,
this.config.rampUpMonths,
implementationCosts.ongoing_monthly
);
// Calculate ROI metrics
const calculations: ROICalculations = {
total_investment: totalInvestment,
net_present_value: this.calculator.calculateNPV(cashFlows, this.config.defaultDiscountRate),
internal_rate_of_return: this.calculator.calculateIRR(cashFlows),
payback_period_months: this.calculator.calculatePaybackPeriod(cashFlows),
five_year_roi: this.calculator.calculate5YearROI(
totalInvestment,
monthlyBenefit,
implementationCosts.ongoing_monthly
),
break_even_date: this.calculator.calculateBreakEvenDate(
new Date(),
this.calculator.calculatePaybackPeriod(cashFlows)
).toISOString()
};
// Generate assumptions
const assumptions = this.generateAssumptions(useCases, implementationCosts);
return {
project_id: projectId,
scenario_name: scenarioName,
metadata: {
confidence_level: 0.95,
assumptions
},
implementation_costs: implementationCosts,
timeline_months: timelineMonths,
financial_metrics: financialMetrics,
calculations
};
}
private aggregateUseCaseMetrics(useCases: UseCase[]): FinancialMetrics {
const metrics: FinancialMetrics = {
monthly_cost_savings: 0,
monthly_time_savings_hours: 0,
quality_improvement_value: 0,
revenue_uplift: 0,
total_monthly_benefit: 0
};
for (const useCase of useCases) {
const { current_state, future_state } = useCase;
// Time savings
const timeSavingsHours = current_state.process_time_hours *
current_state.volume_per_month *
future_state.time_reduction_percentage;
metrics.monthly_time_savings_hours += timeSavingsHours;
// Cost savings (including labor cost from time savings)
const laborCostSavings = timeSavingsHours * FINANCIAL_CONSTANTS.DEFAULT_HOURLY_RATE;
const processCostSavings = current_state.cost_per_transaction *
current_state.volume_per_month *
future_state.automation_percentage;
metrics.monthly_cost_savings += laborCostSavings + processCostSavings;
// Quality improvements (reduction in error costs)
// Note: ERROR_COST_MULTIPLIER is applied to the error rate, not the entire volume
const errorVolume = current_state.volume_per_month * current_state.error_rate;
const errorCostReduction = current_state.cost_per_transaction *
errorVolume *
future_state.error_reduction_percentage *
FINANCIAL_CONSTANTS.ERROR_COST_MULTIPLIER;
metrics.quality_improvement_value += errorCostReduction;
// Revenue uplift from scalability
const scalabilityBenefit = current_state.cost_per_transaction *
current_state.volume_per_month *
(future_state.scalability_factor - 1) *
FINANCIAL_CONSTANTS.SCALABILITY_PROFIT_MARGIN;
metrics.revenue_uplift += scalabilityBenefit;
}
metrics.total_monthly_benefit =
metrics.monthly_cost_savings +
metrics.quality_improvement_value +
metrics.revenue_uplift;
// Log aggregated metrics for debugging
createLogger({ component: 'ROIEngine' }).debug('Aggregated use case metrics', {
use_case_count: useCases.length,
monthly_cost_savings: metrics.monthly_cost_savings,
quality_improvement_value: metrics.quality_improvement_value,
revenue_uplift: metrics.revenue_uplift,
total_monthly_benefit: metrics.total_monthly_benefit
});
return metrics;
}
private calculateFinancialMetrics(
baseMetrics: FinancialMetrics,
confidenceMultiplier: number
): FinancialMetrics {
return {
monthly_cost_savings: baseMetrics.monthly_cost_savings * confidenceMultiplier,
monthly_time_savings_hours: baseMetrics.monthly_time_savings_hours * confidenceMultiplier,
quality_improvement_value: baseMetrics.quality_improvement_value * confidenceMultiplier,
revenue_uplift: baseMetrics.revenue_uplift * confidenceMultiplier,
total_monthly_benefit: baseMetrics.total_monthly_benefit * confidenceMultiplier
};
}
private calculateTotalInvestment(
implementationCosts: ProjectionCreate['implementation_costs'],
useCases: UseCase[]
): number {
// Check if development_hours is project-level or already includes use case hours
// If development_hours is 0 or undefined, assume use case hours are separate
const projectLevelDevelopmentCost = implementationCosts.development_hours * FINANCIAL_CONSTANTS.DEFAULT_DEVELOPER_HOURLY_RATE;
// Only add use case development hours if they're not already included in project development_hours
let useCaseDevelopmentCost = 0;
if (implementationCosts.development_hours === 0) {
// If no project-level hours specified, sum up use case hours
useCaseDevelopmentCost = useCases.reduce(
(total, uc) => total + uc.implementation.development_hours * FINANCIAL_CONSTANTS.DEFAULT_DEVELOPER_HOURLY_RATE,
0
);
}
// Note: If development_hours > 0, we assume it includes all development work
return (
implementationCosts.software_licenses +
projectLevelDevelopmentCost +
implementationCosts.training_costs +
implementationCosts.infrastructure +
useCaseDevelopmentCost
);
}
private generateAssumptions(
useCases: UseCase[],
costs: ProjectionCreate['implementation_costs']
): Array<{ category: string; description: string; impact: 'low' | 'medium' | 'high' }> {
const assumptions = [
{
category: 'Implementation',
description: `${this.config.implementationMonths} months for initial deployment`,
impact: 'high' as const
},
{
category: 'Adoption',
description: `${this.config.rampUpMonths} months to reach full utilization`,
impact: 'medium' as const
},
{
category: 'Labor Cost',
description: `Assumed $${FINANCIAL_CONSTANTS.DEFAULT_HOURLY_RATE}/hour for time savings calculations`,
impact: 'medium' as const
}
];
if (costs.ongoing_monthly > 0) {
assumptions.push({
category: 'Operating Costs',
description: `$${costs.ongoing_monthly.toLocaleString()} monthly ongoing costs`,
impact: 'medium' as const
});
}
// Add use case specific assumptions
const avgComplexity = useCases.reduce((sum, uc) => sum + uc.implementation.complexity_score, 0) / useCases.length;
if (avgComplexity > 7) {
assumptions.push({
category: 'Complexity',
description: 'High implementation complexity may extend timeline',
impact: 'high' as const
});
}
return assumptions;
}
}