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 = {} 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 }) } }