Files
finanzen/convex/lib/helpers.ts
2026-06-15 18:26:25 +02:00

135 lines
3.7 KiB
TypeScript

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<Id<"users">> {
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<string, Id<"categories">>();
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<Id<"categories"> | 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<T extends { userId: Id<"users"> }>(
doc: T | null,
userId: Id<"users">,
label: string,
): Promise<T> {
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);
}