Files
breakpilot-core/pitch-deck/lib/finanzplan/engine.ts
Benjamin Admin dc36e59d17
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m27s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 34s
feat(pitch-deck): formula engine + tooltips for betriebliche Aufwendungen
Engine formulas added:
- Berufsgenossenschaft (F): 2.77% of total brutto payroll (VBG IT rate)
- Internet/Mobilfunk (F): Headcount × 50 EUR/Mon
- Allgemeine Marketingkosten (F): 10% of monthly revenue

UI: Hover tooltips on all (F) and computed rows showing the formula.
SUMME matcher updated for renamed "SUMME Betriebliche Aufwendungen".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:15:16 +02:00

608 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<FPComputeResult> {
// 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])
}
// 5b. Headcount without founders (for formula-based costs)
const NUM_FOUNDERS = 2
const hcWithoutFounders = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) {
hcWithoutFounders[`m${m}`] = Math.max(0, headcount[`m${m}`] - NUM_FOUNDERS)
}
// 5c. Enterprise customers (for Bewirtungskosten)
const kundenRows = await pool.query(
"SELECT segment_name, row_label, values FROM fp_kunden WHERE scenario_id = $1 AND row_label = 'Bestandskunden' ORDER BY sort_order",
[scenarioId]
)
const enterpriseKunden = emptyMonthly()
for (const row of kundenRows.rows) {
if (row.segment_name?.toLowerCase().includes('enterprise')) {
for (let m = 1; m <= MONTHS; m++) {
enterpriseKunden[`m${m}`] = row.values?.[`m${m}`] || 0
}
}
}
// 6. Betriebliche Aufwendungen — compute formula-based rows + sum rows
const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[]
// Formula-based rows: derive from headcount (excl. founders) or customers
const formulaRows: { label: string; perUnit: number; source: MonthlyValues }[] = [
{ label: 'Fort-/Weiterbildungskosten (F)', perUnit: 500, source: hcWithoutFounders },
{ label: 'Fahrzeugkosten (F)', perUnit: 400, source: hcWithoutFounders },
{ label: 'KFZ-Steuern (F)', perUnit: 50, source: hcWithoutFounders },
{ label: 'KFZ-Versicherung (F)', perUnit: 500, source: hcWithoutFounders },
{ label: 'Reisekosten (F)', perUnit: 100, source: headcount },
{ label: 'Bewirtungskosten (F)', perUnit: 200, source: enterpriseKunden },
{ label: 'Internet/Mobilfunk (F)', perUnit: 50, source: headcount },
]
for (const fr of formulaRows) {
const row = betrieb.find(r => r.row_label === fr.label)
if (row) {
const computed = emptyMonthly()
for (let m = 1; 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: 2.77% of total brutto payroll
const bgRow = betrieb.find(r => r.row_label.includes('Berufsgenossenschaft'))
if (bgRow) {
const computed = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) {
computed[`m${m}`] = Math.round((totalBrutto[`m${m}`] || 0) * 0.0277)
}
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), bgRow.id])
bgRow.values = computed
}
// Allgemeine Marketingkosten: 10% of revenue
const marketingRow = betrieb.find(r => r.row_label.includes('Allgemeine Marketingkosten'))
if (marketingRow) {
const computed = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) {
computed[`m${m}`] = Math.round((totalRevenue[`m${m}`] || 0) * 0.10)
}
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), marketingRow.id])
marketingRow.values = computed
}
// Serverkosten: Bestandskunden * 100 + 500 Basis
const totalKunden = emptyMonthly()
for (const row of kundenRows.rows) {
for (let m = 1; m <= MONTHS; m++) {
totalKunden[`m${m}`] += row.values?.[`m${m}`] || 0
}
}
const serverRow = betrieb.find(r => r.row_label === 'Serverkosten Cloud (F)' || r.row_label === 'Serverkosten (Cloud)')
if (serverRow) {
const computed = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) {
computed[`m${m}`] = Math.round((totalKunden[`m${m}`] || 0) * 100 + 500)
}
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), serverRow.id])
serverRow.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(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') || 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}`] = (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('Umsatzerlöse')
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('ÜBERSCHUSS VOR INVESTITIONEN')
const uebVorEnt = findLiq('ÜBERSCHUSS VOR ENTNAHMEN')
const ueberschuss = findLiq('ÜBERSCHUSS')
const kontostand = findLiq('Kontostand zu Beginn des Monats')
const liquiditaet = findLiq('LIQUIDITÄT')
// Operative Einzahlungen (OHNE Eigenkapital und Fremdkapital)
const einzahlungenOperativ = ['Umsatzerlöse', 'Sonst. betriebl. Erträge', 'Anzahlungen']
// Finanzierung (separat)
const finanzierung = ['Neuer Eigenkapitalzugang', 'Erhaltenes Fremdkapital']
// Operative Auszahlungen (OHNE Kreditrückzahlungen)
const auszahlungenOperativ = ['Materialaufwand', 'Personalkosten', 'Sonstige Kosten', 'Umsatzsteuer', 'Gewerbesteuer', 'Körperschaftsteuer']
const finanzAuszahlungen = ['Kreditrückzahlungen']
// 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/Ausschüttungen')
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: 'Umsatzerlöse', values: umsatzAnnual },
{ label: 'Gesamtleistung', values: umsatzAnnual },
{ label: 'Summe Materialaufwand', values: materialAnnual },
{ label: 'Löhne und Gehälter', 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 (Betriebsergebnis)
const ebit: AnnualValues = {}
for (let y = 2026; y <= 2030; y++) {
const k = `y${y}`
ebit[k] = Math.round((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'])
// Steuerberechnung (nur auf Gewinne, mit Verlustvortrag)
// Stockach 78333: Hebesatz 350%
// Gewerbesteuer = 3,5% × 3,5 = 12,25%
// Körperschaftsteuer = 15% + 5,5% Soli = 15,825%
// Gesamt: ~28,075%
const GEWERBESTEUER_RATE = 0.035 * 3.5 // 12,25%
const KOERPERSCHAFTSTEUER_RATE = 0.15 * 1.055 // 15,825% (inkl. Soli)
const gewerbesteuer: AnnualValues = {}
const koerperschaftsteuer: AnnualValues = {}
const steuernGesamt: AnnualValues = {}
const ergebnisNachSteuern: AnnualValues = {}
let verlustvortrag = 0 // kumulierter Verlustvortrag
for (let y = 2026; y <= 2030; y++) {
const k = `y${y}`
const gewinn = ebit[k] || 0
if (gewinn <= 0) {
// Verlust: keine Steuern, Verlustvortrag aufbauen
verlustvortrag += Math.abs(gewinn)
gewerbesteuer[k] = 0
koerperschaftsteuer[k] = 0
steuernGesamt[k] = 0
ergebnisNachSteuern[k] = Math.round(gewinn)
} else {
// Gewinn: Verlustvortrag verrechnen
// Bis 1 Mio EUR: 100% verrechenbar
// Über 1 Mio EUR: nur 60% verrechenbar (Mindestbesteuerung)
let verrechenbar = 0
if (verlustvortrag > 0) {
if (gewinn <= 1000000) {
verrechenbar = Math.min(verlustvortrag, gewinn)
} else {
verrechenbar = Math.min(verlustvortrag, 1000000 + (gewinn - 1000000) * 0.6)
}
verlustvortrag -= verrechenbar
}
const zuVersteuern = Math.max(0, gewinn - verrechenbar)
const gst = Math.round(zuVersteuern * GEWERBESTEUER_RATE)
const kst = Math.round(zuVersteuern * KOERPERSCHAFTSTEUER_RATE)
gewerbesteuer[k] = gst
koerperschaftsteuer[k] = kst
steuernGesamt[k] = gst + kst
ergebnisNachSteuern[k] = Math.round(gewinn - gst - kst)
}
}
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(gewerbesteuer), scenarioId, 'Gewerbesteuer'])
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(koerperschaftsteuer), scenarioId, 'Körperschaftssteuer'])
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(steuernGesamt), scenarioId, 'Steuern gesamt'])
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Ergebnis nach Steuern'])
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Jahresüberschuss'])
// Steuern auch in Liquidität eintragen (monatlich = 1/12 des Jahresbetrags)
const liqGewSt = findLiq('Gewerbesteuer')
const liqKSt = findLiq('Körperschaftsteuer')
if (liqGewSt) {
const v = emptyMonthly()
for (let y = 2026; y <= 2030; y++) {
const jahresBetrag = gewerbesteuer[`y${y}`] || 0
if (jahresBetrag > 0) {
const monatlich = Math.round(jahresBetrag / 12)
const startM = (y - 2026) * 12 + 1
for (let m = startM; m <= startM + 11 && m <= MONTHS; m++) {
v[`m${m}`] = monatlich
}
}
}
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(v), liqGewSt.id])
liqGewSt.values = v
}
if (liqKSt) {
const v = emptyMonthly()
for (let y = 2026; y <= 2030; y++) {
const jahresBetrag = koerperschaftsteuer[`y${y}`] || 0
if (jahresBetrag > 0) {
const monatlich = Math.round(jahresBetrag / 12)
const startM = (y - 2026) * 12 + 1
for (let m = startM; m <= startM + 11 && m <= MONTHS; m++) {
v[`m${m}`] = monatlich
}
}
}
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(v), liqKSt.id])
liqKSt.values = v
}
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