/** * Finanzplan Compute Engine * * 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) */ import { Pool } from 'pg' import { MonthlyValues, AnnualValues, MONTHS, FOUNDING_MONTH, emptyMonthly, sumMonthly, annualSums, dateToMonth, monthToDate, FPPersonalkosten, FPInvestitionen, FPBetrieblicheAufwendungen, FPLiquiditaet, FPComputeResult } from './types' // --- Sheet Calculators --- export function computePersonalkosten(positions: FPPersonalkosten[]): FPPersonalkosten[] { return positions.map(p => { const brutto = emptyMonthly() const sozial = emptyMonthly() const total = emptyMonthly() if (!p.start_date || !p.brutto_monthly) return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total } const startDate = new Date(p.start_date) const startM = dateToMonth(startDate.getFullYear(), startDate.getMonth() + 1) const endM = p.end_date ? dateToMonth(new Date(p.end_date).getFullYear(), new Date(p.end_date).getMonth() + 1) : MONTHS for (let m = Math.max(1, startM); m <= Math.min(MONTHS, endM); m++) { const { year } = monthToDate(m) const yearsFromStart = year - startDate.getFullYear() const raise = Math.pow(1 + (p.annual_raise_pct || 0) / 100, yearsFromStart) const monthlyBrutto = Math.round(p.brutto_monthly * raise) brutto[`m${m}`] = monthlyBrutto sozial[`m${m}`] = Math.round(monthlyBrutto * (p.ag_sozial_pct || 20.425) / 100) total[`m${m}`] = brutto[`m${m}`] + sozial[`m${m}`] } return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total } }) } export function computeInvestitionen(items: FPInvestitionen[]): FPInvestitionen[] { return items.map(item => { const invest = emptyMonthly() const afa = emptyMonthly() if (!item.purchase_date || !item.purchase_amount) return { ...item, values_invest: invest, values_afa: afa } const d = new Date(item.purchase_date) const purchaseM = dateToMonth(d.getFullYear(), d.getMonth() + 1) if (purchaseM >= 1 && purchaseM <= MONTHS) { invest[`m${purchaseM}`] = item.purchase_amount } // AfA (linear depreciation) if (item.afa_years && item.afa_years > 0) { const afaMonths = item.afa_years * 12 const monthlyAfa = Math.round(item.purchase_amount / afaMonths) for (let m = purchaseM; m < purchaseM + afaMonths && m <= MONTHS; m++) { if (m >= 1) afa[`m${m}`] = monthlyAfa } } else { // GWG: full depreciation in purchase month if (purchaseM >= 1 && purchaseM <= MONTHS) { afa[`m${purchaseM}`] = item.purchase_amount } } return { ...item, values_invest: invest, values_afa: afa } }) } function sumRows(rows: { values: MonthlyValues }[]): MonthlyValues { const result = emptyMonthly() for (const row of rows) { for (let m = 1; m <= MONTHS; m++) { result[`m${m}`] += row.values[`m${m}`] || 0 } } return result } function sumField(rows: { [key: string]: MonthlyValues }[], field: string): MonthlyValues { const result = emptyMonthly() for (const row of rows) { const v = row[field] as MonthlyValues if (!v) continue for (let m = 1; m <= MONTHS; m++) { result[`m${m}`] += v[`m${m}`] || 0 } } return result } // --- 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 = emptyMonthly() for (let m = 1; m <= MONTHS; m++) { headcount[`m${m}`] = personal.filter(p => (p.values_total[`m${m}`] || 0) > 0).length } // Write computed values back to DB 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) const prices = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'price') const quantities = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'quantity') const revenueRows = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'revenue') const totalRevenue = emptyMonthly() // Revenue = quantity × price for each module for (const rev of revenueRows) { if (rev.row_label === 'GESAMTUMSATZ') continue const qty = quantities.find(q => q.row_label === rev.row_label) const price = 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) totalRevenue[`m${m}`] += rev.values[`m${m}`] } await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(rev.values), rev.id]) } } // Update GESAMTUMSATZ 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]) } // 5. Materialaufwand (quantity × unit_cost) — simplified const matCosts = (materialRows.rows as FPMaterialaufwand[]).filter(r => r.section === 'cost') const matUnitCosts = (materialRows.rows as FPMaterialaufwand[]).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) totalMaterial[`m${m}`] += cost.values[`m${m}`] } await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(cost.values), cost.id]) } } 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]) } // 6. Betriebliche Aufwendungen — compute sum rows const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[] // Update Personalkosten row const persBetrieb = betrieb.find(r => r.row_label === 'Personalkosten') if (persBetrieb) { await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), persBetrieb.id]) persBetrieb.values = totalPersonal } // Update Abschreibungen row const abrBetrieb = betrieb.find(r => r.row_label === 'Abschreibungen') if (abrBetrieb) { await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalAfa), abrBetrieb.id]) abrBetrieb.values = totalAfa } // Compute category sums const categories = ['steuern', 'versicherungen', 'besondere', 'marketing', 'sonstige'] for (const cat of categories) { const sumRow = betrieb.find(r => r.category === cat && r.is_sum_row) const detailRows = betrieb.filter(r => r.category === cat && !r.is_sum_row) if (sumRow && detailRows.length > 0) { const s = sumRows(detailRows) await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sumRow.id]) sumRow.values = s } } // Summe sonstige (ohne Personal, Abschreibungen) const sonstSumme = betrieb.find(r => r.row_label.includes('Summe sonstige')) if (sonstSumme) { const nonPersonNonAbr = betrieb.filter(r => r.row_label !== 'Personalkosten' && r.row_label !== 'Abschreibungen' && !r.row_label.includes('Summe sonstige') && !r.row_label.includes('Gesamtkosten') && !r.is_sum_row && r.category !== 'personal' && r.category !== 'abschreibungen' ) const s = sumRows(nonPersonNonAbr) await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sonstSumme.id]) sonstSumme.values = s } // Gesamtkosten const gesamtBetrieb = betrieb.find(r => r.row_label.includes('Gesamtkosten')) const totalSonstige = sonstSumme?.values || emptyMonthly() if (gesamtBetrieb) { const g = emptyMonthly() for (let m = 1; m <= MONTHS; m++) { g[`m${m}`] = (totalPersonal[`m${m}`] || 0) + (totalAfa[`m${m}`] || 0) + (totalSonstige[`m${m}`] || 0) } await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(g), gesamtBetrieb.id]) gesamtBetrieb.values = g } // 7. Liquidität const liquid = liquidRows.rows as FPLiquiditaet[] const findLiq = (label: string) => liquid.find(r => r.row_label === label) // Computed rows const liqUmsatz = findLiq('Umsatzerloese') if (liqUmsatz) { await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), liqUmsatz.id]) liqUmsatz.values = totalRevenue } const liqMaterial = findLiq('Materialaufwand') if (liqMaterial) { await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), liqMaterial.id]) liqMaterial.values = totalMaterial } const liqPersonal = findLiq('Personalkosten') if (liqPersonal) { await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), liqPersonal.id]) liqPersonal.values = totalPersonal } const liqSonstige = findLiq('Sonstige Kosten') if (liqSonstige) { await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalSonstige), liqSonstige.id]) liqSonstige.values = totalSonstige } const liqInvest = findLiq('Investitionen') if (liqInvest) { await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalInvest), liqInvest.id]) liqInvest.values = totalInvest } // Compute sums and rolling balance // WICHTIG: Überschuss = nur operativer Cashflow (ohne Kapitaleinzahlungen) const sumEin = findLiq('Summe EINZAHLUNGEN') const sumAus = findLiq('Summe AUSZAHLUNGEN') const uebVorInv = findLiq('UEBERSCHUSS VOR INVESTITIONEN') const uebVorEnt = findLiq('UEBERSCHUSS VOR ENTNAHMEN') const ueberschuss = findLiq('UEBERSCHUSS') const kontostand = findLiq('Kontostand zu Beginn des Monats') const liquiditaet = findLiq('LIQUIDITAET') // Operative Einzahlungen (OHNE Eigenkapital und Fremdkapital) const einzahlungenOperativ = ['Umsatzerloese', 'Sonst. betriebl. Ertraege', 'Anzahlungen'] // Finanzierung (separat) const finanzierung = ['Neuer Eigenkapitalzugang', 'Erhaltenes Fremdkapital'] // Operative Auszahlungen (OHNE Kreditrückzahlungen) const auszahlungenOperativ = ['Materialaufwand', 'Personalkosten', 'Sonstige Kosten', 'Umsatzsteuer', 'Gewerbesteuer', 'Koerperschaftsteuer'] const finanzAuszahlungen = ['Kreditrueckzahlungen'] // Summe EINZAHLUNGEN = nur operativ (für die Zeile "Summe Einzahlungen") if (sumEin) { const s = emptyMonthly() for (const label of einzahlungenOperativ) { const row = findLiq(label) if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0) } await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumEin.id]) sumEin.values = s } // Summe AUSZAHLUNGEN = nur operativ if (sumAus) { const s = emptyMonthly() for (const label of auszahlungenOperativ) { const row = findLiq(label) if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0) } await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id]) sumAus.values = s } // OPERATIVER ÜBERSCHUSS VOR INVESTITIONEN = operative Einzahlungen - operative Auszahlungen if (uebVorInv && sumEin && sumAus) { const s = emptyMonthly() for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((sumEin.values[`m${m}`] || 0) - (sumAus.values[`m${m}`] || 0)) await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id]) uebVorInv.values = s } // ÜBERSCHUSS VOR ENTNAHMEN = Operativer Überschuss - Investitionen if (uebVorEnt && uebVorInv && liqInvest) { const s = emptyMonthly() for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorInv.values[`m${m}`] || 0) - (liqInvest.values[`m${m}`] || 0)) await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id]) uebVorEnt.values = s } // ÜBERSCHUSS = Überschuss vor Entnahmen - Entnahmen (immer noch rein operativ) const entnahmen = findLiq('Kapitalentnahmen/Ausschuettungen') if (ueberschuss && uebVorEnt && entnahmen) { const s = emptyMonthly() for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorEnt.values[`m${m}`] || 0) - (entnahmen.values[`m${m}`] || 0)) await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id]) ueberschuss.values = s } // Rolling Kontostand: Vormonat + Operativer Überschuss + Finanzierung // Finanzierung = Eigenkapital + Fremdkapital - Kreditrückzahlungen if (kontostand && liquiditaet && ueberschuss) { // Berechne monatliche Finanzierungs-Cashflows const finCF = emptyMonthly() for (const label of finanzierung) { const row = findLiq(label) if (row) for (let m = 1; m <= MONTHS; m++) finCF[`m${m}`] += Math.round(row.values[`m${m}`] || 0) } for (const label of finanzAuszahlungen) { const row = findLiq(label) if (row) for (let m = 1; m <= MONTHS; m++) finCF[`m${m}`] -= Math.round(row.values[`m${m}`] || 0) } const ks = emptyMonthly() const lq = emptyMonthly() for (let m = 1; m <= MONTHS; m++) { ks[`m${m}`] = m === 1 ? 0 : Math.round(lq[`m${m - 1}`]) // LIQUIDITÄT = Kontostand + Operativer Überschuss + Finanzierung lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0) + (finCF[`m${m}`] || 0)) } await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ks), kontostand.id]) await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(lq), liquiditaet.id]) kontostand.values = ks liquiditaet.values = lq } // 8. GuV — compute annual values const guv: AnnualValues[] = [] const umsatzAnnual = annualSums(totalRevenue) const materialAnnual = annualSums(totalMaterial) const personalBruttoAnnual = annualSums(totalBrutto) const personalSozialAnnual = annualSums(totalSozial) const personalAnnual = annualSums(totalPersonal) const afaAnnual = annualSums(totalAfa) const sonstigeAnnual = annualSums(totalSonstige) // Write GuV rows const guvUpdates: { label: string; values: AnnualValues }[] = [ { label: 'Umsatzerloese', values: umsatzAnnual }, { label: 'Gesamtleistung', values: umsatzAnnual }, { label: 'Summe Materialaufwand', values: materialAnnual }, { label: 'Loehne und Gehaelter', values: personalBruttoAnnual }, { label: 'Soziale Abgaben', values: personalSozialAnnual }, { label: 'Summe Personalaufwand', values: personalAnnual }, { label: 'Abschreibungen', values: afaAnnual }, { label: 'Sonst. betriebl. Aufwendungen', values: sonstigeAnnual }, ] for (const { label, values } of guvUpdates) { await pool.query( 'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(values), scenarioId, label] ) } // EBIT const ebit: AnnualValues = {} for (let y = 2026; y <= 2030; y++) { const k = `y${y}` ebit[k] = (umsatzAnnual[k] || 0) - (materialAnnual[k] || 0) - (personalAnnual[k] || 0) - (afaAnnual[k] || 0) - (sonstigeAnnual[k] || 0) } await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'EBIT']) await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'Ergebnis nach Steuern']) await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'Jahresueberschuss']) 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: gesamtBetrieb?.values || emptyMonthly() }, liquiditaet: { rows: liquid, endstand: liquiditaet?.values || emptyMonthly() }, guv: [ebit], } } // Import to fix type errors type FPUmsatzerloese = import('./types').FPUmsatzerloese type FPMaterialaufwand = import('./types').FPMaterialaufwand