import { action, query } from "../_generated/server"; import { v } from "convex/values"; import { internal } from "../_generated/api"; import { requireUserId } from "../lib/helpers"; import { getAuthUserId } from "@convex-dev/auth/server"; import type { Doc, Id } from "../_generated/dataModel"; const balanceProvider = v.union(v.literal("comdirect"), v.literal("fints")); const balanceStatus = v.union( v.literal("fresh"), v.literal("stale"), v.literal("error"), v.literal("missing"), ); export const listLatest = query({ args: { includeArchived: v.optional(v.boolean()), }, returns: v.array( v.object({ accountId: v.id("accounts"), accountName: v.string(), accountType: v.string(), iban: v.optional(v.string()), externalId: v.optional(v.string()), balance: v.union(v.number(), v.null()), currency: v.string(), provider: v.union(balanceProvider, v.null()), fetchedAt: v.union(v.number(), v.null()), asOf: v.optional(v.string()), status: balanceStatus, errorMessage: v.optional(v.string()), }), ), handler: async (ctx, args) => { const userId = await requireUserId(ctx); const accounts = await ctx.db .query("accounts") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); const visibleAccounts = accounts .filter((account) => args.includeArchived || !account.isArchived) .sort((a, b) => a.name.localeCompare(b.name)); const balances = await ctx.db .query("accountBalances") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); const latestByAccount = new Map, Doc<"accountBalances">>(); for (const balance of balances.sort((a, b) => b.fetchedAt - a.fetchedAt)) { if (!latestByAccount.has(balance.accountId)) { latestByAccount.set(balance.accountId, balance); } } return visibleAccounts.map((account) => { const latest = latestByAccount.get(account._id); if (!latest) { return { accountId: account._id, accountName: account.name, accountType: account.type, iban: account.iban, externalId: account.externalId, balance: null, currency: account.currency, provider: null, fetchedAt: null, status: "missing" as const, }; } return { accountId: account._id, accountName: account.name, accountType: account.type, iban: account.iban, externalId: account.externalId ?? latest.externalId, balance: latest.balance, currency: latest.currency, provider: latest.provider, fetchedAt: latest.fetchedAt, asOf: latest.asOf, status: latest.status, errorMessage: latest.errorMessage, }; }); }, }); export const refresh = action({ args: { accountId: v.optional(v.id("accounts")), pin: v.optional(v.string()), }, returns: v.object({ updatedCount: v.number(), provider: balanceProvider, awaitingTan: v.boolean(), errors: v.array( v.object({ accountId: v.optional(v.id("accounts")), externalId: v.optional(v.string()), message: v.string(), }), ), }), handler: async ( ctx, args, ): Promise<{ updatedCount: number; provider: "comdirect" | "fints"; awaitingTan: boolean; errors: Array<{ accountId?: Id<"accounts">; externalId?: string; message: string; }>; }> => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Nicht angemeldet"); return await ctx.runAction(internal.bank.orchestrator.refreshBalancesInternal, { userId, accountId: args.accountId, pin: args.pin, }); }, });