Files
breakpilot-core/pitch-deck/lib/finanzplan/engine-betrieb.ts
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

161 lines
6.6 KiB
TypeScript

/**
* 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(),
}
}