Files
breakpilot-core/pitch-deck/lib/finanzplan/engine-guv.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

167 lines
6.2 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.
/**
* GuV (Gewinn- und Verlustrechnung) — annual P&L computation
*
* Computes annual sums, EBIT, taxes (Gewerbesteuer, Körperschaftsteuer)
* with Verlustvortrag, and writes tax amounts back to Liquidität.
*/
import { Pool } from 'pg'
import {
MonthlyValues, AnnualValues, MONTHS,
emptyMonthly, annualSums,
FPLiquiditaet,
} from './types'
export interface GuvContext {
totalRevenue: MonthlyValues
totalMaterial: MonthlyValues
totalBrutto: MonthlyValues
totalSozial: MonthlyValues
totalPersonal: MonthlyValues
totalAfa: MonthlyValues
totalSonstige: MonthlyValues
}
/**
* Compute GuV annual values, taxes, and write tax values to Liquidität.
* Returns EBIT annual values.
*/
export async function computeGuV(
pool: Pool,
scenarioId: string,
liquid: FPLiquiditaet[],
ctx: GuvContext,
): Promise<AnnualValues[]> {
const findLiq = (label: string) => liquid.find(r => r.row_label === label)
const umsatzAnnual = annualSums(ctx.totalRevenue)
const materialAnnual = annualSums(ctx.totalMaterial)
const personalBruttoAnnual = annualSums(ctx.totalBrutto)
const personalSozialAnnual = annualSums(ctx.totalSozial)
const personalAnnual = annualSums(ctx.totalPersonal)
const afaAnnual = annualSums(ctx.totalAfa)
const sonstigeAnnual = annualSums(ctx.totalSonstige)
// Rohergebnis = Gesamtleistung - Materialaufwand
const rohergebnis: AnnualValues = {}
for (let y = 2026; y <= 2030; y++) {
const k = `y${y}`
rohergebnis[k] = Math.round((umsatzAnnual[k] || 0) - (materialAnnual[k] || 0))
}
const guvUpdates: { label: string; values: AnnualValues }[] = [
{ label: 'Umsatzerlöse', values: umsatzAnnual },
{ label: 'Gesamtleistung', values: umsatzAnnual },
{ label: 'Summe Materialaufwand', values: materialAnnual },
{ label: 'Rohergebnis', values: rohergebnis },
{ 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'])
// Tax computation with Verlustvortrag
const { gewerbesteuer, koerperschaftsteuer, steuernGesamt, ergebnisNachSteuern } = computeTaxes(ebit)
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'])
// Write taxes to Liquidität (monthly = 1/12 of annual amount)
await writeTaxToLiquiditaet(pool, findLiq('Gewerbesteuer'), gewerbesteuer)
await writeTaxToLiquiditaet(pool, findLiq('Körperschaftsteuer'), koerperschaftsteuer)
return [ebit]
}
// --- Tax helpers ---
// Stockach 78333: Hebesatz 350%
// Gewerbesteuer = 3,5% × 3,5 = 12,25%
// Körperschaftsteuer = 15% + 5,5% Soli = 15,825%
const GEWERBESTEUER_RATE = 0.035 * 3.5 // 12,25%
const KOERPERSCHAFTSTEUER_RATE = 0.15 * 1.055 // 15,825% (inkl. Soli)
function computeTaxes(ebit: AnnualValues) {
const gewerbesteuer: AnnualValues = {}
const koerperschaftsteuer: AnnualValues = {}
const steuernGesamt: AnnualValues = {}
const ergebnisNachSteuern: AnnualValues = {}
let verlustvortrag = 0
for (let y = 2026; y <= 2030; y++) {
const k = `y${y}`
const gewinn = ebit[k] || 0
if (gewinn <= 0) {
verlustvortrag += Math.abs(gewinn)
gewerbesteuer[k] = 0
koerperschaftsteuer[k] = 0
steuernGesamt[k] = 0
ergebnisNachSteuern[k] = Math.round(gewinn)
} else {
// 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)
}
}
return { gewerbesteuer, koerperschaftsteuer, steuernGesamt, ergebnisNachSteuern }
}
async function writeTaxToLiquiditaet(
pool: Pool,
liqRow: FPLiquiditaet | undefined,
annualTax: AnnualValues,
): Promise<void> {
if (!liqRow) return
const v = emptyMonthly()
for (let y = 2026; y <= 2030; y++) {
const jahresBetrag = annualTax[`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), liqRow.id])
liqRow.values = v
}