add core revenue to model and allow to tweak params

This commit is contained in:
Tobias Brunner 2025-07-23 14:29:54 +02:00
parent a07788cb74
commit 491dbacda4
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
7 changed files with 476 additions and 118 deletions

View file

@ -78,12 +78,17 @@ class ROICalculator {
const graceElement = document.getElementById('grace-period');
const gracePeriod = graceElement ? parseInt(graceElement.value) || 6 : 6;
// Get core service revenue with validation
const coreRevenueElement = document.getElementById('core-service-revenue');
const coreServiceRevenue = coreRevenueElement ? parseFloat(coreRevenueElement.value) || 0 : 0;
return {
investmentAmount,
timeframe,
investmentModel,
loanInterestRate,
revenuePerInstance,
coreServiceRevenue,
servalaShare,
gracePeriod
};
@ -96,6 +101,7 @@ class ROICalculator {
investmentModel: 'direct',
loanInterestRate: 0.05,
revenuePerInstance: 50,
coreServiceRevenue: 0,
servalaShare: 0.25,
gracePeriod: 6
};
@ -204,7 +210,10 @@ class ROICalculator {
monthlyRevenue = monthlyLoanPayment;
} else {
// Direct investment model: Revenue based on instances with performance incentives
monthlyRevenue = currentInstances * inputs.revenuePerInstance;
// Service revenue (shared with Servala) + Core service revenue (100% to CSP)
const serviceRevenue = currentInstances * inputs.revenuePerInstance;
const coreRevenue = currentInstances * inputs.coreServiceRevenue;
monthlyRevenue = serviceRevenue;
// Calculate performance bonus if CSP exceeds baseline expectations
if (baselineInstances > 0 && month > 6) { // Start performance tracking after 6 months
@ -217,11 +226,13 @@ class ROICalculator {
// Determine revenue split based on dynamic grace period
if (month <= effectiveGracePeriod) {
cspRevenue = monthlyRevenue;
// During grace period: CSP keeps all service revenue + core revenue
cspRevenue = serviceRevenue + coreRevenue;
servalaRevenue = 0;
} else {
cspRevenue = monthlyRevenue * (1 - adjustedServalaShare);
servalaRevenue = monthlyRevenue * adjustedServalaShare;
// After grace period: CSP keeps share of service revenue + all core revenue
cspRevenue = (serviceRevenue * (1 - adjustedServalaShare)) + coreRevenue;
servalaRevenue = serviceRevenue * adjustedServalaShare;
}
}
@ -244,6 +255,18 @@ class ROICalculator {
breakEvenMonth = month;
}
// Calculate revenue components for data tracking
let serviceRevenueForData, coreRevenueForData, totalRevenueForData;
if (inputs.investmentModel === 'loan') {
serviceRevenueForData = monthlyLoanPayment;
coreRevenueForData = 0;
totalRevenueForData = monthlyLoanPayment;
} else {
serviceRevenueForData = currentInstances * inputs.revenuePerInstance;
coreRevenueForData = currentInstances * inputs.coreServiceRevenue;
totalRevenueForData = serviceRevenueForData + coreRevenueForData;
}
monthlyData.push({
month,
scenario: scenario.name,
@ -251,7 +274,10 @@ class ROICalculator {
churnedInstances,
totalInstances: currentInstances,
baselineInstances,
monthlyRevenue,
serviceRevenue: serviceRevenueForData,
coreRevenue: coreRevenueForData,
totalRevenue: totalRevenueForData,
monthlyRevenue: serviceRevenueForData, // Keep for backward compatibility
cspRevenue,
servalaRevenue,
cumulativeCSPRevenue,

View file

@ -230,7 +230,9 @@ class ExportManager {
const params = [
['Investment Amount', this.formatCHF(inputs.investmentAmount)],
['Investment Timeframe', `${inputs.timeframe} years`],
['Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / month`],
['Service Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / month`],
['Core Service Revenue per Instance', `${this.formatCHF(inputs.coreServiceRevenue)} / month`],
['Total Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance + inputs.coreServiceRevenue)} / month`],
['Loan Interest Rate', `${(inputs.loanInterestRate * 100).toFixed(1)}%`],
['Direct Investment Share', `${(inputs.servalaShare * 100).toFixed(0)}% to Servala`],
['Grace Period', `${inputs.gracePeriod} months`]
@ -526,7 +528,9 @@ class ExportManager {
if (inputs.investmentModel === 'loan') {
csvContent += `Loan Interest Rate (%),${(inputs.loanInterestRate * 100).toFixed(1)}\n`;
}
csvContent += `Revenue per Instance,${inputs.revenuePerInstance}\n`;
csvContent += `Service Revenue per Instance,${inputs.revenuePerInstance}\n`;
csvContent += `Core Service Revenue per Instance,${inputs.coreServiceRevenue}\n`;
csvContent += `Total Revenue per Instance,${inputs.revenuePerInstance + inputs.coreServiceRevenue}\n`;
if (inputs.investmentModel === 'direct') {
csvContent += `Servala Share (%),${(inputs.servalaShare * 100).toFixed(0)}\n`;
csvContent += `Grace Period (months),${inputs.gracePeriod}\n`;

View file

@ -198,6 +198,18 @@ class ROICalculatorApp {
}
}
updateCoreServiceRevenue(value) {
try {
const element = document.getElementById('core-service-revenue');
if (element) {
element.value = value;
this.updateCalculations();
}
} catch (error) {
console.error('Error updating core service revenue:', error);
}
}
updateScenarioChurn(scenarioKey, churnRate) {
try {
if (this.calculator && this.calculator.scenarios[scenarioKey]) {

View file

@ -183,7 +183,9 @@ class UIManager {
<td><span style="color: ${scenarioColor}" class="fw-bold">${data.scenario}</span></td>
<td>${modelBadge}</td>
<td class="text-end">${data.totalInstances ? data.totalInstances.toLocaleString() : '0'}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.monthlyRevenue || 0)}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.serviceRevenue || data.monthlyRevenue || 0)}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.coreRevenue || 0)}</td>
<td class="text-end fw-bold">${this.formatCurrencyDetailed(data.totalRevenue || data.monthlyRevenue || 0)}</td>
<td class="text-end fw-bold">${this.formatCurrencyDetailed(data.cspRevenue || 0)}</td>
<td class="text-end text-muted">${this.formatCurrencyDetailed(data.servalaRevenue || 0)}</td>
<td class="text-end fw-bold ${netPositionClass}">${this.formatCurrencyDetailed(data.netPosition || 0)}</td>