Belege und Bankmuster in Liquiditätsprognose verwalten
Ermöglicht das Öffnen von Belegen aus der Liquiditätsprognose und das Abschließen erkannter regelmäßiger Bankbewegungen, die anschließend aus der Prognose herausgerechnet werden.
This commit is contained in:
@@ -308,8 +308,14 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get('/functions/liquidity-forecast', async (req, reply) => {
|
||||
const { ignoredRecurringKeys } = req.query as { ignoredRecurringKeys?: string }
|
||||
const ignoredKeys = String(ignoredRecurringKeys || "")
|
||||
.split(",")
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
try {
|
||||
return await generateLiquidityForecast(server, req.user.tenant_id)
|
||||
return await generateLiquidityForecast(server, req.user.tenant_id, ignoredKeys)
|
||||
} catch (err) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: "Liquiditätsprognose konnte nicht erstellt werden." })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { zodResponseFormat } from "openai/helpers/zod";
|
||||
import { and, desc, eq, gte } from "drizzle-orm";
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import {
|
||||
bankaccounts,
|
||||
@@ -22,10 +23,12 @@ type ForecastEvent = {
|
||||
label: string;
|
||||
source: ForecastEventSource;
|
||||
sourceId?: number | string | null;
|
||||
recurringKey?: string;
|
||||
confidence?: number;
|
||||
};
|
||||
|
||||
type RecurringCandidate = {
|
||||
key?: string;
|
||||
label: string;
|
||||
amount: number;
|
||||
interval: "weekly" | "monthly" | "quarterly" | "yearly";
|
||||
@@ -56,6 +59,17 @@ const normalizeText = (value: unknown) => String(value || "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
const getRecurringKey = (candidate: RecurringCandidate) => {
|
||||
const rawKey = [
|
||||
Math.sign(candidate.amount),
|
||||
Math.round(Math.abs(candidate.amount) * 100),
|
||||
normalizeText(candidate.label),
|
||||
candidate.interval,
|
||||
].join("|");
|
||||
|
||||
return createHash("sha1").update(rawKey).digest("hex").slice(0, 16);
|
||||
};
|
||||
|
||||
const addInterval = (date: dayjs.Dayjs, interval: RecurringCandidate["interval"]) => {
|
||||
if (interval === "weekly") return date.add(1, "week");
|
||||
if (interval === "quarterly") return date.add(3, "month");
|
||||
@@ -246,6 +260,10 @@ const mergeRecurringCandidates = (heuristic: RecurringCandidate[], ai: Recurring
|
||||
});
|
||||
|
||||
return [...merged.values()]
|
||||
.map((candidate) => ({
|
||||
...candidate,
|
||||
key: getRecurringKey(candidate),
|
||||
}))
|
||||
.filter((candidate) => Math.abs(candidate.amount) >= 1)
|
||||
.sort((a, b) => a.nextDate.localeCompare(b.nextDate));
|
||||
};
|
||||
@@ -263,6 +281,7 @@ const expandRecurringEvents = (recurring: RecurringCandidate[], endDate: dayjs.D
|
||||
amount: roundMoney(candidate.amount),
|
||||
label: candidate.label,
|
||||
source: "recurring_bankstatement",
|
||||
recurringKey: candidate.key,
|
||||
confidence: candidate.confidence,
|
||||
});
|
||||
date = addInterval(date, candidate.interval);
|
||||
@@ -273,7 +292,11 @@ const expandRecurringEvents = (recurring: RecurringCandidate[], endDate: dayjs.D
|
||||
return events;
|
||||
};
|
||||
|
||||
export const generateLiquidityForecast = async (server: FastifyInstance, tenantId: number) => {
|
||||
export const generateLiquidityForecast = async (
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
ignoredRecurringKeys: string[] = []
|
||||
) => {
|
||||
const today = dayjs().startOf("day");
|
||||
const endDate = today.add(FORECAST_DAYS, "day");
|
||||
const historyStart = today.subtract(HISTORY_MONTHS, "month").format("YYYY-MM-DD");
|
||||
@@ -387,7 +410,9 @@ export const generateLiquidityForecast = async (server: FastifyInstance, tenantI
|
||||
|
||||
const heuristicRecurring = detectRecurringHeuristically(activeStatements);
|
||||
const aiRecurring = await detectRecurringWithAi(server, activeStatements);
|
||||
const recurring = mergeRecurringCandidates(heuristicRecurring, aiRecurring);
|
||||
const ignoredRecurringKeySet = new Set(ignoredRecurringKeys.filter(Boolean));
|
||||
const recurring = mergeRecurringCandidates(heuristicRecurring, aiRecurring)
|
||||
.filter((candidate) => !candidate.key || !ignoredRecurringKeySet.has(candidate.key));
|
||||
const events = [
|
||||
...openEvents,
|
||||
...expandRecurringEvents(recurring, endDate),
|
||||
|
||||
Reference in New Issue
Block a user