[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>
This commit is contained in:
160
pitch-deck/lib/finanzplan/engine-betrieb.ts
Normal file
160
pitch-deck/lib/finanzplan/engine-betrieb.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user