@@ -0,0 +1,412 @@
/**
* 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 * 100 ) / 100
brutto [ ` m ${ m } ` ] = monthlyBrutto
sozial [ ` m ${ m } ` ] = Math . round ( monthlyBrutto * ( p . ag_sozial_pct || 20.425 ) / 100 * 100 ) / 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 * 100 ) / 100
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 * 100 ) / 100
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 * 100 ) / 100
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 ] )
}
// 6. Betriebliche Aufwendungen — compute sum rows
const betrieb = betriebRows . rows as FPBetrieblicheAufwendungen [ ]
// 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' ) )
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 ( 'Umsatzerloese' )
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
const sumEin = findLiq ( 'Summe EINZAHLUNGEN' )
const sumAus = findLiq ( 'Summe AUSZAHLUNGEN' )
const uebVorInv = findLiq ( 'UEBERSCHUSS VOR INVESTITIONEN' )
const uebVorEnt = findLiq ( 'UEBERSCHUSS VOR ENTNAHMEN' )
const ueberschuss = findLiq ( 'UEBERSCHUSS' )
const kontostand = findLiq ( 'Kontostand zu Beginn des Monats' )
const liquiditaet = findLiq ( 'LIQUIDITAET' )
const einzahlungen = [ 'Umsatzerloese' , 'Sonst. betriebl. Ertraege' , 'Anzahlungen' , 'Neuer Eigenkapitalzugang' , 'Erhaltenes Fremdkapital' ]
const auszahlungen = [ 'Materialaufwand' , 'Personalkosten' , 'Sonstige Kosten' , 'Kreditrueckzahlungen' , 'Umsatzsteuer' , 'Gewerbesteuer' , 'Koerperschaftsteuer' ]
if ( sumEin ) {
const s = emptyMonthly ( )
for ( const label of einzahlungen ) {
const row = findLiq ( label )
if ( row ) for ( let m = 1 ; m <= MONTHS ; m ++ ) s [ ` m ${ m } ` ] += 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
}
if ( sumAus ) {
const s = emptyMonthly ( )
for ( const label of auszahlungen ) {
const row = findLiq ( label )
if ( row ) for ( let m = 1 ; m <= MONTHS ; m ++ ) s [ ` m ${ m } ` ] += 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
}
// Überschüsse und Kontostand
if ( uebVorInv && sumEin && sumAus ) {
const s = emptyMonthly ( )
for ( let m = 1 ; m <= MONTHS ; m ++ ) s [ ` m ${ m } ` ] = ( 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
}
if ( uebVorEnt && uebVorInv && liqInvest ) {
const s = emptyMonthly ( )
for ( let m = 1 ; m <= MONTHS ; m ++ ) s [ ` m ${ m } ` ] = ( 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
}
const entnahmen = findLiq ( 'Kapitalentnahmen/Ausschuettungen' )
if ( ueberschuss && uebVorEnt && entnahmen ) {
const s = emptyMonthly ( )
for ( let m = 1 ; m <= MONTHS ; m ++ ) s [ ` m ${ m } ` ] = ( 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
if ( kontostand && liquiditaet && ueberschuss ) {
const ks = emptyMonthly ( )
const lq = emptyMonthly ( )
for ( let m = 1 ; m <= MONTHS ; m ++ ) {
ks [ ` m ${ m } ` ] = m === 1 ? 0 : lq [ ` m ${ m - 1 } ` ]
lq [ ` m ${ m } ` ] = ks [ ` m ${ m } ` ] + ( ueberschuss . values [ ` 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 : 'Umsatzerloese' , values : umsatzAnnual } ,
{ label : 'Gesamtleistung' , values : umsatzAnnual } ,
{ label : 'Summe Materialaufwand' , values : materialAnnual } ,
{ label : 'Loehne und Gehaelter' , 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
const ebit : AnnualValues = { }
for ( let y = 2026 ; y <= 2030 ; y ++ ) {
const k = ` y ${ y } `
ebit [ k ] = ( 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' ] )
await pool . query ( 'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3' , [ JSON . stringify ( ebit ) , scenarioId , 'Ergebnis nach Steuern' ] )
await pool . query ( 'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3' , [ JSON . stringify ( ebit ) , scenarioId , 'Jahresueberschuss' ] )
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