Add scenario-modeler-server example
Demo MCP App for SaaS financial planning with: - Interactive sliders for MRR, growth, churn, margin, and costs - 12-month projection charts (MRR, Gross Profit, Net Profit) - 5 pre-built scenario templates (Bootstrapped, VC Rocketship, etc.) - Template comparison overlay for strategy analysis - Light/dark theme support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f81d17f385
commit
79fa10097b
2 changed files with 1743 additions and 0 deletions
72
examples/scenario-modeler-server/src/lib/calculations.ts
Normal file
72
examples/scenario-modeler-server/src/lib/calculations.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import type {
|
||||
ScenarioInputs,
|
||||
MonthlyProjection,
|
||||
ScenarioSummary,
|
||||
} from "../types.ts";
|
||||
|
||||
export function calculateProjections(
|
||||
inputs: ScenarioInputs,
|
||||
): MonthlyProjection[] {
|
||||
const {
|
||||
startingMRR,
|
||||
monthlyGrowthRate,
|
||||
monthlyChurnRate,
|
||||
grossMargin,
|
||||
fixedCosts,
|
||||
} = inputs;
|
||||
|
||||
// Net growth rate (can be negative if churn > growth)
|
||||
const netGrowthRate = (monthlyGrowthRate - monthlyChurnRate) / 100;
|
||||
|
||||
const projections: MonthlyProjection[] = [];
|
||||
let cumulativeRevenue = 0;
|
||||
|
||||
for (let month = 1; month <= 12; month++) {
|
||||
// MRR with compound growth
|
||||
const mrr = startingMRR * Math.pow(1 + netGrowthRate, month);
|
||||
|
||||
// Profit calculations
|
||||
const grossProfit = mrr * (grossMargin / 100);
|
||||
const netProfit = grossProfit - fixedCosts;
|
||||
|
||||
// Cumulative revenue
|
||||
cumulativeRevenue += mrr;
|
||||
|
||||
projections.push({
|
||||
month,
|
||||
mrr,
|
||||
grossProfit,
|
||||
netProfit,
|
||||
cumulativeRevenue,
|
||||
});
|
||||
}
|
||||
|
||||
return projections;
|
||||
}
|
||||
|
||||
export function calculateSummary(
|
||||
projections: MonthlyProjection[],
|
||||
inputs: ScenarioInputs,
|
||||
): ScenarioSummary {
|
||||
const endingMRR = projections[11].mrr;
|
||||
const arr = endingMRR * 12;
|
||||
const totalRevenue = projections.reduce((sum, p) => sum + p.mrr, 0);
|
||||
const totalProfit = projections.reduce((sum, p) => sum + p.netProfit, 0);
|
||||
const mrrGrowthPct =
|
||||
((endingMRR - inputs.startingMRR) / inputs.startingMRR) * 100;
|
||||
const avgMargin = (totalProfit / totalRevenue) * 100;
|
||||
|
||||
// Find first month where netProfit >= 0
|
||||
const breakEvenProjection = projections.find((p) => p.netProfit >= 0);
|
||||
const breakEvenMonth = breakEvenProjection?.month ?? null;
|
||||
|
||||
return {
|
||||
endingMRR,
|
||||
arr,
|
||||
totalRevenue,
|
||||
totalProfit,
|
||||
mrrGrowthPct,
|
||||
avgMargin,
|
||||
breakEvenMonth,
|
||||
};
|
||||
}
|
||||
44
examples/scenario-modeler-server/src/lib/formatters.ts
Normal file
44
examples/scenario-modeler-server/src/lib/formatters.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Format a number as currency with abbreviated suffixes.
|
||||
* Examples: $50K, $1.2M, -$30K
|
||||
*/
|
||||
export function formatCurrency(value: number): string {
|
||||
const absValue = Math.abs(value);
|
||||
const sign = value < 0 ? "-" : "";
|
||||
|
||||
if (absValue >= 1_000_000) {
|
||||
const formatted = (absValue / 1_000_000).toFixed(
|
||||
absValue >= 10_000_000 ? 1 : 2,
|
||||
);
|
||||
// Remove trailing zeros after decimal
|
||||
const cleaned = formatted.replace(/\.?0+$/, "");
|
||||
return `${sign}$${cleaned}M`;
|
||||
}
|
||||
|
||||
if (absValue >= 1_000) {
|
||||
const formatted = (absValue / 1_000).toFixed(absValue >= 100_000 ? 0 : 1);
|
||||
const cleaned = formatted.replace(/\.?0+$/, "");
|
||||
return `${sign}$${cleaned}K`;
|
||||
}
|
||||
|
||||
return `${sign}$${Math.round(absValue)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number as a percentage.
|
||||
* Examples: 5%, -2.5%, +12%
|
||||
*/
|
||||
export function formatPercent(value: number, showSign = false): string {
|
||||
const sign = showSign && value > 0 ? "+" : "";
|
||||
return `${sign}${value.toFixed(1).replace(/\.0$/, "")}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency for slider display (always show K suffix for consistency).
|
||||
*/
|
||||
export function formatCurrencySlider(value: number): string {
|
||||
if (value >= 1_000) {
|
||||
return `$${(value / 1_000).toFixed(0)}K`;
|
||||
}
|
||||
return `$${value}`;
|
||||
}
|
||||
Loading…
Reference in a new issue