perf(pitch-deck): fix slow financial slides — cached results + batch insert

- Compute endpoint now returns cached results if available (single SELECT
  instead of DELETE + 60 INSERTs)
- When recompute is needed, batch all 60 rows into a single INSERT
- Reduces DB calls from 61 to 2 (cached) or 3 (recompute)
- Fixes timeout/blank financial slides for investors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-15 18:43:05 +02:00
parent 7c17e484c1
commit e8a18c0025

View File

@@ -28,6 +28,30 @@ export async function POST(request: NextRequest) {
const client = await pool.connect()
try {
// Fast path: return cached results if they exist (avoid expensive recompute + 60 inserts)
const cached = await client.query(
'SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month',
[scenarioId]
)
if (cached.rows.length > 0) {
const results = cached.rows
const lastResult = results[results.length - 1]
const breakEvenMonth = results.find(r => r.month > 1 && (r.revenue_eur - r.total_costs_eur) >= 0)?.month || null
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.max(...results.map((r: Record<string, number>) => r.burn_rate_eur)),
total_funding_needed: Math.round(Math.abs(Math.min(...results.map((r: Record<string, number>) => r.cash_balance_eur), 0)) * 100) / 100,
},
})
}
// Load assumptions
const assumptionsRes = await client.query(
'SELECT key, value, value_type FROM pitch_fm_assumptions WHERE scenario_id = $1',
@@ -150,19 +174,15 @@ export async function POST(request: NextRequest) {
})
}
// Save to DB (upsert)
// Save to DB (batch insert — single query instead of 60 individual inserts)
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)
`, [
const cols = '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'
const values: unknown[] = []
const placeholders: string[] = []
results.forEach((r, i) => {
const offset = i * 24
placeholders.push(`(${Array.from({length: 24}, (_, j) => `$${offset + j + 1}`).join(',')})`)
values.push(
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,
@@ -170,8 +190,9 @@ export async function POST(request: NextRequest) {
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,
])
}
)
})
await client.query(`INSERT INTO pitch_fm_results (${cols}) VALUES ${placeholders.join(',')}`, values)
const lastResult = results[results.length - 1]
return NextResponse.json({