initial commit

This commit is contained in:
Matthias
2026-06-15 11:33:23 +02:00
commit fc0a6fb975
155 changed files with 24526 additions and 0 deletions

148
convex/comdirect/sync.ts Normal file
View File

@@ -0,0 +1,148 @@
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import type { Id } from "../_generated/dataModel";
import { getAuthUserId } from "@convex-dev/auth/server";
import { getAccountBalances, getTransactions } from "./client";
import { mapComdirectTransaction } from "../lib/comdirectMap";
export const run = action({
args: {
accountId: v.optional(v.id("accounts")),
from: v.string(),
to: v.string(),
},
returns: v.object({
importedCount: v.number(),
skippedCount: v.number(),
}),
handler: async (ctx, args): Promise<{ importedCount: number; skippedCount: number }> => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Nicht angemeldet");
const clientId = process.env.COMDIRECT_CLIENT_ID;
const clientSecret = process.env.COMDIRECT_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error("comdirect API-Zugangsdaten nicht konfiguriert");
}
const session = await ctx.runQuery(internal.comdirect.internal.getSession, { userId });
if (!session?.accessToken || !session.secondaryActive) {
throw new Error("comdirect-Session nicht aktiv. Bitte erneut anmelden.");
}
const accessToken = session.accessToken;
const sessionUuid = session.sessionUuid;
const balances = await getAccountBalances(accessToken, sessionUuid);
const accountIdMap = new Map<string, typeof args.accountId>();
for (const item of balances.values ?? []) {
const account = item.account as {
accountId?: string;
iban?: string;
accountType?: { text?: string };
};
const accountIdExternal = account?.accountId;
if (!accountIdExternal) continue;
const balanceValue = Number((item.balance as { value?: string })?.value ?? 0);
const convexAccountId = await ctx.runMutation(
internal.comdirect.internal.upsertAccountFromComdirect,
{
userId,
externalId: accountIdExternal,
name: account.accountType?.text ?? "comdirect Konto",
iban: account.iban,
balance: balanceValue,
},
);
accountIdMap.set(accountIdExternal, convexAccountId);
}
const settings = await ctx.runQuery(internal.settings.getInternal, { userId });
const ownNames = settings?.ownNames ?? [];
const salaryShift = settings?.salaryShift ?? {
enabled: true,
categoryNames: ["Gehalt & Besoldung"],
dayThreshold: 25,
};
const rows: Array<{
accountId?: typeof args.accountId;
categoryName: string;
bookingDate?: string;
valueDate?: string;
description: string;
counterparty?: string;
amount: number;
vorgang?: string;
isPending: boolean;
rawText?: string;
assignedMonth?: string;
effectiveMonth?: string;
externalRef?: string;
}> = [];
const targetAccounts = args.accountId
? [...accountIdMap.entries()].filter(([, id]) => id === args.accountId)
: [...accountIdMap.entries()];
for (const [externalAccountId, convexAccountId] of targetAccounts) {
for (const state of ["BOOKED", "NOTBOOKED"] as const) {
let offset = 0;
let matches = 0;
do {
const result = await getTransactions(accessToken, sessionUuid, externalAccountId, {
transactionState: state,
pagingFirst: offset,
minBookingDate: args.from,
maxBookingDate: args.to,
});
matches = result.paging.matches;
for (const tx of result.values ?? []) {
const mapped = mapComdirectTransaction(
tx as Parameters<typeof mapComdirectTransaction>[0],
ownNames,
salaryShift,
);
rows.push({
accountId: convexAccountId,
categoryName: mapped.categoryName,
bookingDate: mapped.bookingDate,
valueDate: mapped.valueDate,
description: mapped.description,
counterparty: mapped.counterparty,
amount: mapped.amount,
vorgang: mapped.vorgang,
isPending: mapped.isPending,
rawText: mapped.rawText,
assignedMonth: mapped.assignedMonth,
effectiveMonth: mapped.effectiveMonth,
externalRef: mapped.externalRef,
});
}
offset += result.values?.length ?? 0;
} while (offset < matches);
}
}
const commitResult: {
importId: Id<"imports">;
importedCount: number;
skippedCount: number;
} = await ctx.runMutation(internal.imports.commitRowsInternal, {
userId,
filename: `comdirect-sync-${args.from}-${args.to}`,
source: "comdirect-api",
accountId: args.accountId,
rows,
});
await ctx.runMutation(internal.comdirect.internal.clearSession, { userId });
return {
importedCount: commitResult.importedCount,
skippedCount: commitResult.skippedCount,
};
},
});