This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/pitch-deck/app/api/financial-model/compute/route.ts
BreakPilot Dev b464366341
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
feat: Add staged funding model, financial compute engine, annex slides and UI enhancements
Restructure financial plan from single 200k SAFE to realistic staged funding
(25k Stammkapital, 25k Angel, 200k Wandeldarlehen, 1M Series A = 1.25M total).
Add 60-month compute engine with CAPEX/OPEX accounting, cash constraints,
hardware financing (30% upfront / 70% leasing), and revenue-based hiring caps.
Rebuild TheAskSlide with 4-event funding timeline, update i18n (DE/EN),
chat agent core messages, and add 15 new annex/technology slides with
supporting UI components (KPICard, RunwayGauge, WaterfallChart, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:20:02 +01:00

389 lines
16 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import pool from '@/lib/db'
interface FundingEvent {
month: number
amount: number
label: string
}
interface SalaryStep {
from_month: number
salary: number
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { scenarioId } = body
if (!scenarioId) {
return NextResponse.json({ error: 'scenarioId is required' }, { status: 400 })
}
const client = await pool.connect()
try {
// Load assumptions
const assumptionsRes = await client.query(
'SELECT key, value, value_type FROM pitch_fm_assumptions WHERE scenario_id = $1',
[scenarioId]
)
const a: Record<string, number | number[] | FundingEvent[] | SalaryStep[]> = {}
for (const row of assumptionsRes.rows) {
const val = typeof row.value === 'string' ? JSON.parse(row.value) : row.value
a[row.key] = val
}
// Funding schedule (staged capital injections)
const fundingSchedule: FundingEvent[] = Array.isArray(a.funding_schedule)
? (a.funding_schedule as FundingEvent[])
: [
{ month: 8, amount: 25000, label: 'Stammkapital GmbH' },
{ month: 9, amount: 25000, label: 'Angel-Runde' },
{ month: 10, amount: 200000, label: 'Wandeldarlehen (Investor + L-Bank)' },
{ month: 19, amount: 1000000, label: 'Series A' },
]
// Founder salary schedule (per founder)
const founderSalarySchedule: SalaryStep[] = Array.isArray(a.founder_salary_schedule)
? (a.founder_salary_schedule as SalaryStep[])
: [
{ from_month: 1, salary: 0 },
{ from_month: 10, salary: 3000 },
{ from_month: 13, salary: 6000 },
{ from_month: 25, salary: 10000 },
]
const numFounders = Number(a.num_founders) || 2
// Extract scalar values
const growthRate = (Number(a.monthly_growth_rate) || 15) / 100
const churnRate = (Number(a.churn_rate_monthly) || 3) / 100
const arpuMini = Number(a.arpu_mini) || 299
const arpuStudio = Number(a.arpu_studio) || 999
const arpuCloud = Number(a.arpu_cloud) || 1499
const mixMini = (Number(a.product_mix_mini) || 60) / 100
const mixStudio = (Number(a.product_mix_studio) || 25) / 100
const mixCloud = (Number(a.product_mix_cloud) || 15) / 100
const initialCustomers = Number(a.initial_customers) || 2
const cac = Number(a.cac) || 500
const hwCostMini = Number(a.hw_cost_per_mini) || 3200
const hwCostStudio = Number(a.hw_cost_per_studio) || 12000
const cloudOpex = Number(a.cloud_opex_per_customer) || 150
const salaryAvg = Number(a.salary_avg_monthly) || 6000
// Hiring plan: employees EXCLUDING founders (hired staff only)
const hiringPlan: number[] = Array.isArray(a.hiring_plan) ? (a.hiring_plan as number[]) : [0, 1, 3, 6, 10]
const marketingMonthly = Number(a.marketing_monthly) || 2000
const infraBase = Number(a.infra_monthly_base) || 500
// Detail cost assumptions
const ihkAnnual = Number(a.ihk_annual) || 180
const phoneInternetMonthly = Number(a.phone_internet_monthly) || 100
const taxAdvisorMonthly = Number(a.tax_advisor_monthly) || 300
const notaryFounding = Number(a.notary_founding) || 2500
const insuranceMonthly = Number(a.insurance_monthly) || 200
const officeRentMonthly = Number(a.office_rent_monthly) || 0
const softwareLicensesMonthly = Number(a.software_licenses_monthly) || 150
const travelMonthly = Number(a.travel_monthly) || 200
const legalMonthly = Number(a.legal_monthly) || 100
const depreciationRatePct = Number(a.depreciation_rate_pct) || 33
const taxRatePct = Number(a.tax_rate_pct) || 30
const interestRatePct = Number(a.interest_rate_pct) || 5
// Hardware financing: only this % paid upfront, rest via leasing/financing
const hwUpfrontPct = (Number(a.hw_upfront_pct) || 30) / 100
// GmbH founding month (month 8 = August 2026)
const gmbhFoundingMonth = 8
// Weighted ARPU
const weightedArpu = arpuMini * mixMini + arpuStudio * mixStudio + arpuCloud * mixCloud
// Weighted hardware cost (only for Mini and Studio — Cloud is OpEx)
const hwCostWeighted = hwCostMini * mixMini + hwCostStudio * mixStudio
// Helper: get founder salary for a given month
function getFounderSalary(month: number): number {
let salary = 0
for (const step of founderSalarySchedule) {
if (month >= step.from_month) {
salary = step.salary
}
}
return salary
}
// Helper: get funding for a given month
function getFundingForMonth(month: number): number {
let total = 0
for (const event of fundingSchedule) {
if (event.month === month) {
total += event.amount
}
}
return total
}
const results = []
let totalCustomers = 0
let cashBalance = 0 // Start at 0, funding comes via schedule
let cumulativeRevenue = 0
let breakEvenMonth: number | null = null
let peakBurn = 0
let cumulativeHwInvestment = 0
for (let m = 1; m <= 60; m++) {
const yearIndex = Math.floor((m - 1) / 12) // 0-4
const year = 2026 + yearIndex
const monthInYear = ((m - 1) % 12) + 1
// === FUNDING: Add capital injection for this month ===
const fundingThisMonth = getFundingForMonth(m)
cashBalance += fundingThisMonth
// === PRE-GMBH PHASE (months 1-7): No costs, private development ===
if (m < gmbhFoundingMonth) {
results.push({
month: m,
year,
month_in_year: monthInYear,
new_customers: 0,
churned_customers: 0,
total_customers: 0,
mrr_eur: 0,
arr_eur: 0,
revenue_eur: 0,
cogs_eur: 0,
personnel_eur: 0,
infra_eur: 0,
marketing_eur: 0,
total_costs_eur: 0,
employees_count: 0,
gross_margin_pct: 0,
burn_rate_eur: 0,
runway_months: 999,
cac_eur: 0,
ltv_eur: 0,
ltv_cac_ratio: 0,
cash_balance_eur: Math.round(cashBalance * 100) / 100,
cumulative_revenue_eur: 0,
admin_costs_eur: 0,
office_costs_eur: 0,
founding_costs_eur: 0,
ihk_eur: 0,
depreciation_eur: 0,
interest_expense_eur: 0,
taxes_eur: 0,
net_income_eur: 0,
ebit_eur: 0,
software_licenses_eur: 0,
travel_costs_eur: 0,
funding_eur: fundingThisMonth,
})
continue
}
// === POST-GMBH PHASE (months 8+): Real business operations ===
// Hired employees: plan-based but capped by revenue (don't hire ahead of revenue)
const plannedHires = hiringPlan[Math.min(yearIndex, hiringPlan.length - 1)] || 0
const revenueBasedMaxHires = Math.floor((totalCustomers * weightedArpu) / salaryAvg)
const hiredEmployees = Math.min(plannedHires, Math.max(0, revenueBasedMaxHires))
// Founder salary
const founderSalaryPerPerson = getFounderSalary(m)
const totalFounderSalary = founderSalaryPerPerson * numFounders
// Total employees shown (founders + hired)
const totalEmployees = numFounders + hiredEmployees
// Customer dynamics — start acquiring customers from GmbH founding
const monthsSinceGmbh = m - gmbhFoundingMonth + 1
if (monthsSinceGmbh === 1) {
totalCustomers = initialCustomers
}
let newCustomers = monthsSinceGmbh === 1
? initialCustomers
: Math.max(1, Math.round(totalCustomers * growthRate))
// Cash constraint: don't spend more than available
// Fixed OPEX this month (independent of new customer count)
const fixedOpex = (totalFounderSalary + hiredEmployees * salaryAvg)
+ marketingMonthly
+ infraBase + (totalCustomers * 5)
+ (totalCustomers * mixCloud * cloudOpex)
+ phoneInternetMonthly + taxAdvisorMonthly + insuranceMonthly + legalMonthly
+ officeRentMonthly + (m === gmbhFoundingMonth ? notaryFounding : 0)
+ ihkAnnual / 12 + softwareLicensesMonthly + travelMonthly
const estRevenue = totalCustomers * weightedArpu
// Available cash = current balance + this month's revenue - fixed costs
const availableCash = cashBalance + estRevenue - fixedOpex
// Variable cost per new customer: hardware CAPEX (upfront portion) + CAC
const varCostPerNew = hwCostWeighted * hwUpfrontPct + cac
// Max affordable new customers (keep cash >= 0)
if (varCostPerNew > 0 && monthsSinceGmbh > 1) {
const maxAffordable = Math.floor(availableCash / varCostPerNew)
newCustomers = Math.min(newCustomers, Math.max(1, maxAffordable))
}
const churned = Math.round(totalCustomers * churnRate)
if (monthsSinceGmbh > 1) {
totalCustomers = totalCustomers + newCustomers - churned
}
totalCustomers = Math.max(0, totalCustomers)
// Revenue
const mrr = totalCustomers * weightedArpu
const arr = mrr * 12
const revenue = mrr
// Costs
const hiredPersonnelCost = hiredEmployees * salaryAvg
const personnelCost = totalFounderSalary + hiredPersonnelCost
// Hardware = CAPEX (only upfront portion paid from cash, rest financed)
const capexHardware = newCustomers * hwCostWeighted * hwUpfrontPct
// Cloud OPEX + hardware leasing cost (financed portion amortized over 36 months)
const cogsCloud = totalCustomers * mixCloud * cloudOpex
const hwLeasingMonthly = (cumulativeHwInvestment * (1 - hwUpfrontPct)) / 36
const cogs = cogsCloud + hwLeasingMonthly
const marketingCost = marketingMonthly + (newCustomers * cac)
const infraCost = infraBase + (totalCustomers * 5)
// Detail costs
const adminCosts = phoneInternetMonthly + taxAdvisorMonthly + insuranceMonthly + legalMonthly
const officeCosts = officeRentMonthly
// Founding costs: notary in month 8 (GmbH founding)
const foundingCosts = m === gmbhFoundingMonth ? notaryFounding : 0
const ihkMonthly = ihkAnnual / 12
const softwareLicenses = softwareLicensesMonthly
const travelCosts = travelMonthly
// Depreciation: cumulative HW investment * rate / 12 (P&L expense for CAPEX)
cumulativeHwInvestment += capexHardware
const depreciationMonthly = (cumulativeHwInvestment * depreciationRatePct / 100) / 12
// Total OPEX (P&L) — hardware enters only via depreciation
const totalCosts = personnelCost + cogs + marketingCost + infraCost
+ adminCosts + officeCosts + foundingCosts + ihkMonthly
+ softwareLicenses + travelCosts + depreciationMonthly
// EBIT
const ebit = revenue - totalCosts
// Interest expense (only if cash balance is negative)
const interestExpense = cashBalance < 0 ? Math.abs(cashBalance) * interestRatePct / 100 / 12 : 0
// Taxes (only if profit positive)
const ebt = ebit - interestExpense
const taxes = ebt > 0 ? ebt * taxRatePct / 100 : 0
// Net income
const netIncome = ebt - taxes
// Cash: net income MINUS hardware CAPEX (funding already added at top)
cashBalance += netIncome - capexHardware
cumulativeRevenue += revenue
// KPIs — gross margin uses COGS + depreciation for true margin
const grossMargin = revenue > 0 ? ((revenue - cogs - depreciationMonthly) / revenue) * 100 : 0
const burnRate = (netIncome - capexHardware) < 0 ? Math.abs(netIncome - capexHardware) : 0
const runway = burnRate > 0 ? cashBalance / burnRate : 999
const avgLifetimeMonths = churnRate > 0 ? 1 / churnRate : 60
const ltv = weightedArpu * avgLifetimeMonths
const ltvCacRatio = cac > 0 ? ltv / cac : 0
if (peakBurn < burnRate) peakBurn = burnRate
// Break-even detection
if (breakEvenMonth === null && netIncome >= 0 && m > gmbhFoundingMonth) {
breakEvenMonth = m
}
results.push({
month: m,
year,
month_in_year: monthInYear,
new_customers: newCustomers,
churned_customers: churned,
total_customers: totalCustomers,
mrr_eur: Math.round(mrr * 100) / 100,
arr_eur: Math.round(arr * 100) / 100,
revenue_eur: Math.round(revenue * 100) / 100,
cogs_eur: Math.round(cogs * 100) / 100,
personnel_eur: Math.round(personnelCost * 100) / 100,
infra_eur: Math.round(infraCost * 100) / 100,
marketing_eur: Math.round(marketingCost * 100) / 100,
total_costs_eur: Math.round(totalCosts * 100) / 100,
employees_count: totalEmployees,
gross_margin_pct: Math.round(grossMargin * 100) / 100,
burn_rate_eur: Math.round(burnRate * 100) / 100,
runway_months: Math.round(Math.min(runway, 999) * 10) / 10,
cac_eur: cac,
ltv_eur: Math.round(ltv * 100) / 100,
ltv_cac_ratio: Math.round(ltvCacRatio * 100) / 100,
cash_balance_eur: Math.round(cashBalance * 100) / 100,
cumulative_revenue_eur: Math.round(cumulativeRevenue * 100) / 100,
// Detail costs
admin_costs_eur: Math.round(adminCosts * 100) / 100,
office_costs_eur: Math.round(officeCosts * 100) / 100,
founding_costs_eur: Math.round(foundingCosts * 100) / 100,
ihk_eur: Math.round(ihkMonthly * 100) / 100,
depreciation_eur: Math.round(depreciationMonthly * 100) / 100,
interest_expense_eur: Math.round(interestExpense * 100) / 100,
taxes_eur: Math.round(taxes * 100) / 100,
net_income_eur: Math.round(netIncome * 100) / 100,
ebit_eur: Math.round(ebit * 100) / 100,
software_licenses_eur: Math.round(softwareLicenses * 100) / 100,
travel_costs_eur: Math.round(travelCosts * 100) / 100,
funding_eur: fundingThisMonth,
})
}
// Save to DB (upsert) — only columns that exist in the table
await client.query('DELETE FROM pitch_fm_results WHERE scenario_id = $1', [scenarioId])
for (const r of results) {
await client.query(`
INSERT INTO pitch_fm_results (scenario_id, month, year, month_in_year,
new_customers, churned_customers, total_customers,
mrr_eur, arr_eur, revenue_eur,
cogs_eur, personnel_eur, infra_eur, marketing_eur, total_costs_eur,
employees_count, gross_margin_pct, burn_rate_eur, runway_months,
cac_eur, ltv_eur, ltv_cac_ratio,
cash_balance_eur, cumulative_revenue_eur)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
`, [
scenarioId, r.month, r.year, r.month_in_year,
r.new_customers, r.churned_customers, r.total_customers,
r.mrr_eur, r.arr_eur, r.revenue_eur,
r.cogs_eur, r.personnel_eur, r.infra_eur, r.marketing_eur, r.total_costs_eur,
r.employees_count, r.gross_margin_pct, r.burn_rate_eur, r.runway_months,
r.cac_eur, r.ltv_eur, r.ltv_cac_ratio,
r.cash_balance_eur, r.cumulative_revenue_eur,
])
}
const lastResult = results[results.length - 1]
return NextResponse.json({
scenario_id: scenarioId,
results,
summary: {
final_arr: lastResult.arr_eur,
final_customers: lastResult.total_customers,
break_even_month: breakEvenMonth,
final_runway: lastResult.runway_months,
final_ltv_cac: lastResult.ltv_cac_ratio,
peak_burn: Math.round(peakBurn * 100) / 100,
total_funding_needed: Math.round(Math.abs(Math.min(...results.map(r => r.cash_balance_eur), 0)) * 100) / 100,
},
})
} finally {
client.release()
}
} catch (error) {
console.error('Compute error:', error)
return NextResponse.json({ error: 'Computation failed' }, { status: 500 })
}
}