/** * 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 { 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 } }