[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:
166
pitch-deck/lib/finanzplan/engine-guv.ts
Normal file
166
pitch-deck/lib/finanzplan/engine-guv.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user