- archive.entities.list / archive.entities.restore für alle per MCP archivierbaren Entitäten mit Berechtigungsprüfung je Entität und Historieneintrag - accounting.bank_accounts.balances liefert Stände von Bankkonten und Kassenbüchern, optional zu einem Stichtag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.7 KiB
TypeScript
72 lines
3.7 KiB
TypeScript
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<typeof bankaccounts.$inferSelect, "id" | "name" | "iban" | "ownerName" | "bankId" | "datevNumber" | "balance" | "expired" | "syncedAt">
|
|
|
|
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<string>`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<number, number>()
|
|
|
|
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.",
|
|
},
|
|
}
|
|
}
|