fix(pitch-deck): replace all hardcoded financial numbers with computed values
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 40s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 37s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 33s
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 40s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 37s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 33s
All financial data now flows from the same compute engine (useFinancialModel). No more hardcoded numbers in any slide — all values are derived from the finanzplan database, ensuring consistency across all pitch deck versions. - FinanzplanSlide: KPI table + charts now use computeAnnualKPIs() from FMResult[] - BusinessModelSlide: bottom-up calc (customers × ACV = ARR) from compute engine - AssumptionsSlide: Base case from compute, Bear/Bull scaled from Base - New helper: lib/finanzplan/annual-kpis.ts for 60-month → 5-year aggregation - PitchDeck: passes investorId to all financial slides for version-aware data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
pitch-deck/lib/finanzplan/annual-kpis.ts
Normal file
89
pitch-deck/lib/finanzplan/annual-kpis.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { FMResult } from '../types'
|
||||
|
||||
export interface AnnualKPI {
|
||||
year: number
|
||||
mrr: number
|
||||
arr: number
|
||||
customers: number
|
||||
arpu: number
|
||||
employees: number
|
||||
revenuePerEmployee: number
|
||||
personnelCosts: number
|
||||
totalRevenue: number
|
||||
totalCosts: number
|
||||
ebit: number
|
||||
ebitMargin: number
|
||||
taxes: number
|
||||
netIncome: number
|
||||
serverCostPerCustomer: number
|
||||
grossMargin: number
|
||||
burnRate: number
|
||||
runway: number | null
|
||||
cashBalance: number
|
||||
}
|
||||
|
||||
const TAX_RATE = 0.30 // ~30% Körperschaftsteuer + Gewerbesteuer + Soli
|
||||
|
||||
/**
|
||||
* Aggregates 60 monthly FMResult entries into 5 annual KPI rows (2026–2030).
|
||||
* All values are derived — nothing is hardcoded.
|
||||
*/
|
||||
export function computeAnnualKPIs(results: FMResult[]): AnnualKPI[] {
|
||||
if (!results || results.length === 0) return []
|
||||
|
||||
const years = [2026, 2027, 2028, 2029, 2030]
|
||||
|
||||
return years.map(year => {
|
||||
const yearResults = results.filter(r => r.year === year)
|
||||
if (yearResults.length === 0) {
|
||||
return emptyKPI(year)
|
||||
}
|
||||
|
||||
const dec = yearResults[yearResults.length - 1] // December snapshot
|
||||
const totalRevenue = yearResults.reduce((s, r) => s + r.revenue_eur, 0)
|
||||
const personnelCosts = yearResults.reduce((s, r) => s + r.personnel_eur, 0)
|
||||
const totalCogs = yearResults.reduce((s, r) => s + r.cogs_eur, 0)
|
||||
const totalInfra = yearResults.reduce((s, r) => s + r.infra_eur, 0)
|
||||
const totalMarketing = yearResults.reduce((s, r) => s + r.marketing_eur, 0)
|
||||
const totalCosts = yearResults.reduce((s, r) => s + r.total_costs_eur, 0)
|
||||
|
||||
const ebit = totalRevenue - totalCosts
|
||||
const ebitMargin = totalRevenue > 0 ? (ebit / totalRevenue) * 100 : 0
|
||||
const taxes = ebit > 0 ? Math.round(ebit * TAX_RATE) : 0
|
||||
const netIncome = ebit - taxes
|
||||
const serverCost = dec.total_customers > 0
|
||||
? Math.round((totalInfra / 12) / dec.total_customers)
|
||||
: 0
|
||||
|
||||
return {
|
||||
year,
|
||||
mrr: Math.round(dec.mrr_eur),
|
||||
arr: Math.round(dec.arr_eur),
|
||||
customers: Math.round(dec.total_customers),
|
||||
arpu: dec.total_customers > 0 ? Math.round(dec.mrr_eur / dec.total_customers) : 0,
|
||||
employees: Math.round(dec.employees_count),
|
||||
revenuePerEmployee: dec.employees_count > 0 ? Math.round(totalRevenue / dec.employees_count) : 0,
|
||||
personnelCosts: Math.round(personnelCosts),
|
||||
totalRevenue: Math.round(totalRevenue),
|
||||
totalCosts: Math.round(totalCosts),
|
||||
ebit: Math.round(ebit),
|
||||
ebitMargin: Math.round(ebitMargin),
|
||||
taxes,
|
||||
netIncome: Math.round(netIncome),
|
||||
serverCostPerCustomer: serverCost,
|
||||
grossMargin: Math.round(dec.gross_margin_pct),
|
||||
burnRate: Math.round(dec.burn_rate_eur),
|
||||
runway: dec.runway_months > 999 ? null : Math.round(dec.runway_months),
|
||||
cashBalance: Math.round(dec.cash_balance_eur),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function emptyKPI(year: number): AnnualKPI {
|
||||
return {
|
||||
year, mrr: 0, arr: 0, customers: 0, arpu: 0, employees: 0,
|
||||
revenuePerEmployee: 0, personnelCosts: 0, totalRevenue: 0, totalCosts: 0,
|
||||
ebit: 0, ebitMargin: 0, taxes: 0, netIncome: 0,
|
||||
serverCostPerCustomer: 0, grossMargin: 0, burnRate: 0, runway: null, cashBalance: 0,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user