/** * Finanzplan Compute Engine — Orchestrator * * Dependency order: * Personalkosten (independent inputs) * Investitionen (independent inputs) * Kunden → Umsatzerlöse → Materialaufwand * Betriebliche Aufwendungen (needs Personal + Invest) * Sonst. betr. Erträge (independent) * Liquidität (aggregates all above) * GuV (annual summary) * * Split into modules: * engine-sheets.ts — pure calculators (no DB) * engine-betrieb.ts — betriebliche aufwendungen * engine-liquiditaet.ts — liquidity / cash flow * engine-guv.ts — GuV / P&L + taxes */ import { Pool } from 'pg' import { MonthlyValues, MONTHS, FOUNDING_MONTH, emptyMonthly, FPPersonalkosten, FPInvestitionen, FPBetrieblicheAufwendungen, FPLiquiditaet, FPComputeResult, } from './types' import { computePersonalkosten, computeInvestitionen, sumField, computeHeadcount, } from './engine-sheets' import { computeBetrieblicheAufwendungen } from './engine-betrieb' import { computeLiquiditaet } from './engine-liquiditaet' import { computeGuV } from './engine-guv' // Re-export sheet calculators for direct consumers export { computePersonalkosten, computeInvestitionen } from './engine-sheets' // Import types used inline type FPUmsatzerloese = import('./types').FPUmsatzerloese type FPMaterialaufwand = import('./types').FPMaterialaufwand // --- Main Engine --- export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise { // 1. Load all editable data from DB const [ personalRows, investRows, betriebRows, liquidRows, kundenSummary, umsatzRows, materialRows, ] = await Promise.all([ pool.query('SELECT * FROM fp_personalkosten WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_investitionen WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_betriebliche_aufwendungen WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_liquiditaet WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_kunden_summary WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_umsatzerloese WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), pool.query('SELECT * FROM fp_materialaufwand WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), ]) // 2. Compute Personalkosten const personal = computePersonalkosten(personalRows.rows as FPPersonalkosten[]) const totalBrutto = sumField(personal as any, 'values_brutto') const totalSozial = sumField(personal as any, 'values_sozial') const totalPersonal = sumField(personal as any, 'values_total') const headcount = computeHeadcount(personal) for (const p of personal) { await pool.query( 'UPDATE fp_personalkosten SET values_brutto = $1, values_sozial = $2, values_total = $3 WHERE id = $4', [JSON.stringify(p.values_brutto), JSON.stringify(p.values_sozial), JSON.stringify(p.values_total), p.id] ) } // 3. Compute Investitionen const invest = computeInvestitionen(investRows.rows as FPInvestitionen[]) const totalInvest = sumField(invest as any, 'values_invest') const totalAfa = sumField(invest as any, 'values_afa') for (const i of invest) { await pool.query( 'UPDATE fp_investitionen SET values_invest = $1, values_afa = $2 WHERE id = $3', [JSON.stringify(i.values_invest), JSON.stringify(i.values_afa), i.id] ) } // 4. Umsatzerlöse (quantity × price) + Materialaufwand const { totalRevenue, totalMaterial } = await computeRevenueAndMaterial( pool, umsatzRows.rows as FPUmsatzerloese[], materialRows.rows as FPMaterialaufwand[] ) // 5. Bestandskunden (for formula-based costs) const kundenRows = await pool.query( "SELECT segment_name, row_label, values FROM fp_kunden WHERE scenario_id = $1 AND row_label LIKE 'Bestandskunden%' ORDER BY sort_order", [scenarioId] ) const totalBestandskunden = emptyMonthly() for (const row of kundenRows.rows) { const rl = (row as { row_label?: string }).row_label || '' if (rl.includes('Bestandskunden') && !rl.includes('gesamt')) { for (let m = 1; m <= MONTHS; m++) { totalBestandskunden[`m${m}`] += row.values?.[`m${m}`] || 0 } } } // Cloud-Hosting in Materialaufwand const matRows = materialRows.rows as FPMaterialaufwand[] const cloudRow = matRows.find(r => r.row_label.includes('Cloud-Hosting')) if (cloudRow) { const computed = emptyMonthly() for (let m = FOUNDING_MONTH; m <= MONTHS; m++) { const kunden = totalBestandskunden[`m${m}`] || 0 const extraKunden = Math.max(0, kunden - 10) computed[`m${m}`] = Math.round(extraKunden * 100 + 1500) } await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(computed), cloudRow.id]) cloudRow.values = computed } // 6. Betriebliche Aufwendungen const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[] const { totalSonstige, totalGesamt } = await computeBetrieblicheAufwendungen(pool, betrieb, { totalBrutto, totalPersonal, totalAfa, totalRevenue, totalMaterial, headcount, totalBestandskunden, }) // 7. Liquidität const liquid = liquidRows.rows as FPLiquiditaet[] const { endstand } = await computeLiquiditaet(pool, liquid, { totalRevenue, totalMaterial, totalPersonal, totalSonstige, totalInvest, }) // 8. GuV const guv = await computeGuV(pool, scenarioId, liquid, { totalRevenue, totalMaterial, totalBrutto, totalSozial, totalPersonal, totalAfa, totalSonstige, }) return { personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount }, investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest }, umsatzerloese: { total: totalRevenue }, materialaufwand: { total: totalMaterial }, betriebliche: { total_sonstige: totalSonstige, total_gesamt: totalGesamt }, liquiditaet: { rows: liquid, endstand }, guv, } } // --- Revenue & Material helpers (kept here to avoid circular deps) --- async function computeRevenueAndMaterial( pool: Pool, umsatzAllRows: FPUmsatzerloese[], materialAllRows: FPMaterialaufwand[], ): Promise<{ totalRevenue: MonthlyValues; totalMaterial: MonthlyValues }> { const prices = umsatzAllRows.filter(r => r.section === 'price') const quantities = umsatzAllRows.filter(r => r.section === 'quantity') const revenueRows = umsatzAllRows.filter(r => r.section === 'revenue') const totalRevenue = emptyMonthly() const extractTier = (label: string) => { const m = label.match(/\(([^)]+)\)/); return m ? m[1] : label } for (const rev of revenueRows) { if (rev.row_label === 'GESAMTUMSATZ') continue const tier = extractTier(rev.row_label) const qty = quantities.find(q => extractTier(q.row_label) === tier) || quantities.find(q => q.row_label === rev.row_label) const price = prices.find(p => extractTier(p.row_label) === tier) || prices.find(p => p.row_label === rev.row_label) if (qty && price) { for (let m = 1; m <= MONTHS; m++) { const v = (qty.values[`m${m}`] || 0) * (price.values[`m${m}`] || 0) rev.values[`m${m}`] = Math.round(v) } await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(rev.values), rev.id]) } for (let m = 1; m <= MONTHS; m++) { totalRevenue[`m${m}`] += rev.values[`m${m}`] || 0 } } const gesamtUmsatz = revenueRows.find(r => r.row_label === 'GESAMTUMSATZ') if (gesamtUmsatz) { await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), gesamtUmsatz.id]) } // Materialaufwand const matCosts = materialAllRows.filter(r => r.section === 'cost') const matUnitCosts = materialAllRows.filter(r => r.section === 'unit_cost') const totalMaterial = emptyMonthly() for (const cost of matCosts) { if (cost.row_label === 'SUMME') continue const uc = matUnitCosts.find(u => u.row_label === cost.row_label) const qty = quantities.find(q => q.row_label === cost.row_label) if (uc && qty) { for (let m = 1; m <= MONTHS; m++) { const v = (qty.values[`m${m}`] || 0) * (uc.values[`m${m}`] || 0) cost.values[`m${m}`] = Math.round(v) } await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(cost.values), cost.id]) } for (let m = 1; m <= MONTHS; m++) { totalMaterial[`m${m}`] += cost.values[`m${m}`] || 0 } } const matSumme = matCosts.find(r => r.row_label === 'SUMME') if (matSumme) { await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), matSumme.id]) } return { totalRevenue, totalMaterial } }