From e8a18c0025cd1376f71a952b96e6e0b705342aab Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 15 Apr 2026 18:43:05 +0200 Subject: [PATCH] =?UTF-8?q?perf(pitch-deck):=20fix=20slow=20financial=20sl?= =?UTF-8?q?ides=20=E2=80=94=20cached=20results=20+=20batch=20insert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../app/api/financial-model/compute/route.ts | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/pitch-deck/app/api/financial-model/compute/route.ts b/pitch-deck/app/api/financial-model/compute/route.ts index 0517618..4008acf 100644 --- a/pitch-deck/app/api/financial-model/compute/route.ts +++ b/pitch-deck/app/api/financial-model/compute/route.ts @@ -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) => r.burn_rate_eur)), + total_funding_needed: Math.round(Math.abs(Math.min(...results.map((r: Record) => 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({