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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user