135 lines
3.7 KiB
TypeScript
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);
|
|
}
|