/** * Betriebliche Aufwendungen — formula-based rows + category sums * * Computes formula-driven operating expenses (Fortbildung, Reisekosten, etc.), * Gewerbesteuer, category sums, and Gesamtkosten. */ import { Pool } from 'pg' import { MonthlyValues, MONTHS, FOUNDING_MONTH, emptyMonthly, FPBetrieblicheAufwendungen, } from './types' import { sumRows } from './engine-sheets' export interface BetriebContext { totalBrutto: MonthlyValues totalPersonal: MonthlyValues totalAfa: MonthlyValues totalRevenue: MonthlyValues totalMaterial: MonthlyValues headcount: MonthlyValues totalBestandskunden: MonthlyValues } /** * Compute all formula-based betriebliche aufwendungen rows and sums. * Writes computed values back to DB. */ export async function computeBetrieblicheAufwendungen( pool: Pool, betrieb: FPBetrieblicheAufwendungen[], ctx: BetriebContext, ): Promise<{ totalSonstige: MonthlyValues; totalGesamt: MonthlyValues }> { const NUM_FOUNDERS = 2 const hcWithoutFounders = emptyMonthly() for (let m = 1; m <= MONTHS; m++) { hcWithoutFounders[`m${m}`] = Math.max(0, ctx.headcount[`m${m}`] - NUM_FOUNDERS) } // Formula-based rows: derive from headcount (excl. founders) or customers const formulaRows: { label: string; perUnit: number; source: MonthlyValues }[] = [ { label: 'Fort-/Weiterbildungskosten (F)', perUnit: 300, source: hcWithoutFounders }, { label: 'Reisekosten (F)', perUnit: 75, source: ctx.headcount }, { label: 'Bewirtungskosten (F)', perUnit: 50, source: ctx.totalBestandskunden }, { label: 'Internet/Mobilfunk (F)', perUnit: 50, source: ctx.headcount }, ] for (const fr of formulaRows) { const row = betrieb.find(r => r.row_label === fr.label) if (row) { const computed = emptyMonthly() for (let m = FOUNDING_MONTH; m <= MONTHS; m++) { computed[`m${m}`] = Math.round((fr.source[`m${m}`] || 0) * fr.perUnit) } await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), row.id]) row.values = computed } } // Berufsgenossenschaft (VBG IT/Büro): ~0.5% of total brutto payroll const bgRow = betrieb.find(r => r.row_label.includes('Berufsgenossenschaft')) if (bgRow) { const computed = emptyMonthly() for (let m = FOUNDING_MONTH; m <= MONTHS; m++) { computed[`m${m}`] = Math.round((ctx.totalBrutto[`m${m}`] || 0) * 0.005) } await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), bgRow.id]) bgRow.values = computed } // Allgemeine Marketingkosten: 8% of revenue (2026-2028), 10% from 2029 const marketingRow = betrieb.find(r => r.row_label.includes('Allgemeine Marketingkosten')) if (marketingRow) { const computed = emptyMonthly() for (let m = FOUNDING_MONTH; m <= MONTHS; m++) { const rate = m <= 36 ? 0.08 : 0.10 // m36 = Dec 2028 computed[`m${m}`] = Math.round((ctx.totalRevenue[`m${m}`] || 0) * rate) } await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), marketingRow.id]) marketingRow.values = computed } // 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(ctx.totalPersonal), persBetrieb.id]) persBetrieb.values = ctx.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(ctx.totalAfa), abrBetrieb.id]) abrBetrieb.values = ctx.totalAfa } // Gewerbesteuer (F): 12.25% of monthly profit (only when positive) const gewStRow = betrieb.find(r => r.row_label.includes('Gewerbesteuer')) if (gewStRow) { const nonTaxOpex = betrieb.filter(r => r.category !== 'steuern' && r.category !== 'personal' && r.category !== 'abschreibungen' && !r.is_sum_row && !r.row_label.includes('Summe') && !r.row_label.includes('SUMME') ) const computed = emptyMonthly() for (let m = FOUNDING_MONTH; m <= MONTHS; m++) { const rev = ctx.totalRevenue[`m${m}`] || 0 const mat = ctx.totalMaterial[`m${m}`] || 0 const pers = ctx.totalPersonal[`m${m}`] || 0 const afa = ctx.totalAfa[`m${m}`] || 0 let opex = 0 for (const r of nonTaxOpex) { opex += r.values[`m${m}`] || 0 } const profit = rev - mat - pers - afa - opex computed[`m${m}`] = profit > 0 ? Math.round(profit * 0.1225) : 0 } await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), gewStRow.id]) gewStRow.values = computed } // 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') || r.row_label.includes('SUMME Betriebliche')) const totalSonstige = sonstSumme?.values || emptyMonthly() if (gesamtBetrieb) { const g = emptyMonthly() for (let m = 1; m <= MONTHS; m++) { g[`m${m}`] = (ctx.totalPersonal[`m${m}`] || 0) + (ctx.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 } return { totalSonstige, totalGesamt: gesamtBetrieb?.values || emptyMonthly(), } }