import { getAuthUserId } from "@convex-dev/auth/server"; import type { ActionCtx, MutationCtx, QueryCtx } from "../_generated/server"; import type { Id } from "../_generated/dataModel"; import { categorize, roundEur } from "./categorize"; import { computeEffectiveMonth, resolveAssignedAndEffective } from "./month"; import { computeDedupHash } from "./comdirectMap"; export async function requireUserId(ctx: QueryCtx | MutationCtx | ActionCtx): Promise> { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Nicht angemeldet"); return userId; } export async function getAppSettings(ctx: QueryCtx | MutationCtx, userId: Id<"users">) { return await ctx.db .query("appSettings") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); } export async function getCategoryMap(ctx: QueryCtx | MutationCtx, userId: Id<"users">) { const categories = await ctx.db .query("categories") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); const byName = new Map>(); for (const cat of categories) { byName.set(cat.name, cat._id); } return byName; } export async function resolveCategoryId( ctx: QueryCtx | MutationCtx, userId: Id<"users">, categoryName: string, ): Promise | undefined> { const cat = await ctx.db .query("categories") .withIndex("by_user_name", (q) => q.eq("userId", userId).eq("name", categoryName)) .unique(); return cat?._id; } export type TransactionInput = { accountId?: Id<"accounts">; categoryId?: Id<"categories">; categoryName?: string; bookingDate?: string; valueDate?: string; description: string; counterparty?: string; amount: number; vorgang?: string; isPending: boolean; notes?: string; rawText?: string; importId?: Id<"imports">; assignedMonth?: string; externalRef?: string; }; export async function enrichTransactionFields( ctx: MutationCtx, userId: Id<"users">, input: TransactionInput, ) { const settings = await getAppSettings(ctx, userId); const salaryShift = settings?.salaryShift ?? { enabled: true, categoryNames: ["Gehalt & Besoldung"], dayThreshold: 25, }; const ownNames = settings?.ownNames ?? []; let categoryId = input.categoryId; let categoryName = input.categoryName; if (!categoryId && !categoryName && input.rawText) { categoryName = categorize( input.rawText, input.amount, input.vorgang ?? "", ownNames, ); } if (!categoryId && categoryName) { categoryId = await resolveCategoryId(ctx, userId, categoryName); } if (categoryId && !categoryName) { const cat = await ctx.db.get("categories", categoryId); categoryName = cat?.name; } const { assignedMonth, effectiveMonth } = resolveAssignedAndEffective( input.bookingDate, input.amount, categoryName, salaryShift, input.assignedMonth, ); const dedupHash = await computeDedupHash({ accountId: input.accountId, bookingDate: input.bookingDate, amount: roundEur(input.amount), description: input.description, vorgang: input.vorgang, }); return { categoryId, assignedMonth, effectiveMonth, dedupHash, amount: roundEur(input.amount), }; } export async function assertOwned }>( doc: T | null, userId: Id<"users">, label: string, ): Promise { if (!doc) throw new Error(`${label} nicht gefunden`); if (doc.userId !== userId) throw new Error("Nicht autorisiert"); return doc; } export function recomputeEffectiveMonth( bookingDate: string | undefined, assignedMonth: string | undefined, ) { return computeEffectiveMonth(bookingDate, assignedMonth); }