feat(mcp): Archivierte Einträge wiederherstellen und Kontostände abfragen
- 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>
This commit is contained in:
71
backend/src/modules/bank-balances.service.ts
Normal file
71
backend/src/modules/bank-balances.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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.",
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user