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(); 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[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, }; }, });