Add bank synchronization features with FinTS support and update dependencies
This commit is contained in:
@@ -1,148 +1,37 @@
|
||||
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";
|
||||
|
||||
/** @deprecated Nutze api.bank.sync.run – leitet an den Orchestrator mit FinTS-Fallback weiter. */
|
||||
export const run = action({
|
||||
args: {
|
||||
accountId: v.optional(v.id("accounts")),
|
||||
from: v.string(),
|
||||
to: v.string(),
|
||||
pin: v.optional(v.string()),
|
||||
},
|
||||
returns: v.object({
|
||||
importedCount: v.number(),
|
||||
skippedCount: v.number(),
|
||||
provider: v.union(v.literal("comdirect"), v.literal("fints")),
|
||||
awaitingTan: v.boolean(),
|
||||
}),
|
||||
handler: async (ctx, args): Promise<{ importedCount: number; skippedCount: number }> => {
|
||||
handler: async (ctx, args): Promise<{
|
||||
importedCount: number;
|
||||
skippedCount: number;
|
||||
provider: "comdirect" | "fints";
|
||||
awaitingTan: boolean;
|
||||
}> => {
|
||||
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, {
|
||||
return await ctx.runAction(internal.bank.orchestrator.runSyncInternal, {
|
||||
userId,
|
||||
filename: `comdirect-sync-${args.from}-${args.to}`,
|
||||
source: "comdirect-api",
|
||||
from: args.from,
|
||||
to: args.to,
|
||||
accountId: args.accountId,
|
||||
rows,
|
||||
pin: args.pin,
|
||||
});
|
||||
|
||||
await ctx.runMutation(internal.comdirect.internal.clearSession, { userId });
|
||||
|
||||
return {
|
||||
importedCount: commitResult.importedCount,
|
||||
skippedCount: commitResult.skippedCount,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user