Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m13s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 49s
CI / test-python-voice (push) Successful in 38s
CI / test-bqas (push) Successful in 31s
# Conflicts: # pitch-deck/components/slides/MilestonesSlide.tsx # pitch-deck/lib/finanzplan/engine.ts
151 lines
6.3 KiB
TypeScript
151 lines
6.3 KiB
TypeScript
/**
|
|
* Liquiditaet — rolling cash balance computation
|
|
*
|
|
* Computes Einzahlungen/Auszahlungen sums (dynamic row_type-based),
|
|
* Ueberschuss vor Investitionen/Entnahmen, and rolling Kontostand/Liquiditaet.
|
|
*/
|
|
|
|
import { Pool } from 'pg'
|
|
import {
|
|
MonthlyValues, MONTHS,
|
|
emptyMonthly,
|
|
FPLiquiditaet,
|
|
} from './types'
|
|
|
|
export interface LiquiditaetContext {
|
|
totalRevenue: MonthlyValues
|
|
totalMaterial: MonthlyValues
|
|
totalPersonal: MonthlyValues
|
|
totalSonstige: MonthlyValues
|
|
totalInvest: MonthlyValues
|
|
}
|
|
|
|
/**
|
|
* Compute all liquidity rows and rolling balance.
|
|
* Writes computed values back to DB.
|
|
*/
|
|
export async function computeLiquiditaet(
|
|
pool: Pool,
|
|
liquid: FPLiquiditaet[],
|
|
ctx: LiquiditaetContext,
|
|
): Promise<{ endstand: MonthlyValues }> {
|
|
const findLiq = (label: string) => liquid.find(r => r.row_label === label)
|
|
|
|
// Computed rows — link to computed totals
|
|
const liqUmsatz = findLiq('Umsatzerlöse')
|
|
if (liqUmsatz) {
|
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalRevenue), liqUmsatz.id])
|
|
liqUmsatz.values = ctx.totalRevenue
|
|
}
|
|
const liqMaterial = findLiq('Materialaufwand')
|
|
if (liqMaterial) {
|
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalMaterial), liqMaterial.id])
|
|
liqMaterial.values = ctx.totalMaterial
|
|
}
|
|
const liqPersonal = findLiq('Personalkosten')
|
|
if (liqPersonal) {
|
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalPersonal), liqPersonal.id])
|
|
liqPersonal.values = ctx.totalPersonal
|
|
}
|
|
const liqSonstige = findLiq('Sonstige Kosten')
|
|
if (liqSonstige) {
|
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalSonstige), liqSonstige.id])
|
|
liqSonstige.values = ctx.totalSonstige
|
|
}
|
|
const liqInvest = findLiq('Investitionen')
|
|
if (liqInvest) {
|
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalInvest), liqInvest.id])
|
|
liqInvest.values = ctx.totalInvest
|
|
}
|
|
|
|
// Compute sums and rolling balance — dynamic row_type-based (handles any label conventions)
|
|
await computeRollingBalance(pool, liquid)
|
|
|
|
return { endstand: liquid.find(r => r.row_type === 'kontostand' && r.row_label.includes('LIQUIDIT'))?.values || emptyMonthly() }
|
|
}
|
|
|
|
/**
|
|
* Recompute Summe AUSZAHLUNGEN -> UEBERSCHUSS chain -> rolling balance.
|
|
* Called both in initial pass and after tax values are written from GuV.
|
|
*/
|
|
export async function computeRollingBalance(
|
|
pool: Pool,
|
|
liquid: FPLiquiditaet[],
|
|
): Promise<void> {
|
|
const findLiqMatch = (options: string[]) => liquid.find(r => options.includes(r.row_label))
|
|
|
|
const sumEin = findLiqMatch(['Summe ERTRÄGE', 'Summe EINZAHLUNGEN'])
|
|
const sumAus = findLiqMatch(['Summe AUSZAHLUNGEN'])
|
|
const uebVorInv = findLiqMatch(['ÜBERSCHUSS VOR INVESTITIONEN', 'UEBERSCHUSS VOR INVESTITIONEN'])
|
|
const uebVorEnt = findLiqMatch(['ÜBERSCHUSS VOR ENTNAHMEN', 'UEBERSCHUSS VOR ENTNAHMEN'])
|
|
const ueberschuss = findLiqMatch(['ÜBERSCHUSS', 'UEBERSCHUSS'])
|
|
const liqInvest = liquid.find(r => r.row_label === 'Investitionen')
|
|
// Kontostand: label varies per scenario (with/without parentheses)
|
|
const kontostand = liquid.find(r => r.row_type === 'kontostand' && !r.row_label.includes('LIQUIDIT'))
|
|
const liquiditaet = liquid.find(r => r.row_type === 'kontostand' && r.row_label.includes('LIQUIDIT'))
|
|
|
|
// Summe ERTRAEGE = ALL einzahlungen (dynamic — works regardless of how many rows exist)
|
|
if (sumEin) {
|
|
const s = emptyMonthly()
|
|
for (const row of liquid) {
|
|
if (row.row_type === 'einzahlung' && row.id !== sumEin.id) {
|
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(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
|
|
}
|
|
|
|
// Summe AUSZAHLUNGEN = ALL auszahlungen (dynamic)
|
|
if (sumAus) {
|
|
const s = emptyMonthly()
|
|
for (const row of liquid) {
|
|
if (row.row_type === 'auszahlung' && row.id !== sumAus.id) {
|
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(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
|
|
}
|
|
|
|
// UEBERSCHUSS VOR INVESTITIONEN = Summe ERTRAEGE - Summe AUSZAHLUNGEN (total cashflow)
|
|
if (uebVorInv && sumEin && sumAus) {
|
|
const s = emptyMonthly()
|
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((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
|
|
}
|
|
|
|
// UEBERSCHUSS VOR ENTNAHMEN = UEBERSCHUSS VOR INVESTITIONEN - Investitionen
|
|
if (uebVorEnt && uebVorInv && liqInvest) {
|
|
const s = emptyMonthly()
|
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((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
|
|
}
|
|
|
|
// UEBERSCHUSS = UEBERSCHUSS VOR ENTNAHMEN - Kapitalentnahmen
|
|
const entnahmen = findLiqMatch(['Kapitalentnahmen/Ausschüttungen', 'Kapitalentnahmen/Ausschuettungen'])
|
|
if (ueberschuss && uebVorEnt && entnahmen) {
|
|
const s = emptyMonthly()
|
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((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 balance: LIQUIDITAET[m] = LIQUIDITAET[m-1] + UEBERSCHUSS[m]
|
|
// UEBERSCHUSS now includes ALL cash flows (operative + financing + repayments)
|
|
if (kontostand && liquiditaet && ueberschuss) {
|
|
const ks = emptyMonthly()
|
|
const lq = emptyMonthly()
|
|
for (let m = 1; m <= MONTHS; m++) {
|
|
ks[`m${m}`] = m === 1 ? 0 : Math.round(lq[`m${m - 1}`])
|
|
lq[`m${m}`] = Math.round(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
|
|
}
|
|
}
|