import { FastifyInstance } from "fastify" import { and, eq, sql } from "drizzle-orm" import { bankaccounts, bankstatements } from "../../db/schema" export const CASHBOOK_BANK_ID = "fedeo-cashbook" type BalanceAccount = Pick const roundMoney = (value: number) => Math.round(value * 100) / 100 /** * Bank accounts store the last synced booked balance; cashbooks store their opening balance * and derive the current balance from their bookings. For a past date, bank balances are * reconstructed by reverting the statements booked after that date. */ export function calculateAccountBalance(account: BalanceAccount, statementSum: number, asOf: string | null) { const cashbook = account.bankId === CASHBOOK_BANK_ID const stored = Number(account.balance || 0) const balance = cashbook ? stored + statementSum : stored - statementSum return { id: account.id, type: cashbook ? "cashbook" : "bank", name: account.name || account.ownerName || (cashbook ? "Kassenbuch" : "Bankkonto"), iban: cashbook ? null : account.iban, datevNumber: account.datevNumber, expired: account.expired, balance: roundMoney(balance), currency: "EUR", ...(cashbook ? { openingBalance: roundMoney(stored) } : { syncedBalance: roundMoney(stored), syncedAt: account.syncedAt }), source: cashbook ? "cashbook_bookings" : asOf ? "bank_sync_reconstructed" : "bank_sync", } } export async function loadBankBalances(server: FastifyInstance, tenantId: number, options: { account?: number | null; asOf?: string | null } = {}) { const asOf = options.asOf || null const conditions = [eq(bankaccounts.tenant, tenantId), eq(bankaccounts.archived, false)] if (options.account) conditions.push(eq(bankaccounts.id, options.account)) const accounts = await server.db.select().from(bankaccounts).where(and(...conditions)).orderBy(bankaccounts.name, bankaccounts.id) const sums = async (dateCondition: any) => { const rows = await server.db .select({ account: bankstatements.account, total: sql`coalesce(sum(${bankstatements.amount}), 0)` }) .from(bankstatements) .where(and(eq(bankstatements.tenant, tenantId), eq(bankstatements.archived, false), ...(dateCondition ? [dateCondition] : []))) .groupBy(bankstatements.account) return new Map(rows.map((row) => [row.account, Number(row.total)])) } // Synced statements may carry a full timestamp; compare the calendar day only. const bookingDay = sql`left(${bankstatements.date}, 10)` // Cashbooks need bookings up to the date, bank accounts only those after it. const cashbookSums = await sums(asOf ? sql`${bookingDay} <= ${asOf}` : null) const bankSums = asOf ? await sums(sql`${bookingDay} > ${asOf}`) : new Map() const balances = accounts.map((account) => calculateAccountBalance( account, (account.bankId === CASHBOOK_BANK_ID ? cashbookSums : bankSums).get(account.id) || 0, asOf, )) const active = balances.filter((account) => !account.expired) return { asOf, accounts: balances, totals: { balance: roundMoney(active.reduce((sum, account) => sum + account.balance, 0)), bank: roundMoney(active.filter((account) => account.type === "bank").reduce((sum, account) => sum + account.balance, 0)), cashbook: roundMoney(active.filter((account) => account.type === "cashbook").reduce((sum, account) => sum + account.balance, 0)), currency: "EUR", note: "Summen ohne abgelaufene Bankverbindungen.", }, } }