import { query, mutation } from "../_generated/server"; import { v } from "convex/values"; import { requireUserId } from "../lib/helpers"; const bankConfigReturn = v.object({ _id: v.id("bankConfig"), _creationTime: v.number(), userId: v.id("users"), providerPreference: v.union( v.literal("auto"), v.literal("comdirect"), v.literal("fints"), ), comdirectHasCredentials: v.boolean(), fints: v.object({ blz: v.string(), url: v.string(), login: v.string(), productId: v.string(), productVersion: v.optional(v.string()), tanMethodId: v.optional(v.number()), tanMediaName: v.optional(v.string()), }), }); export const getConfig = query({ args: {}, returns: v.union(bankConfigReturn, v.null()), handler: async (ctx) => { const userId = await requireUserId(ctx); const config = await ctx.db .query("bankConfig") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); if (!config) return null; const { bankingInformationJson: _omit, ...rest } = config.fints; return { ...config, fints: rest, }; }, }); export const getSyncState = query({ args: {}, returns: v.union( v.object({ _id: v.id("syncState"), _creationTime: v.number(), userId: v.id("users"), lastSync: v.optional(v.number()), lastProviderUsed: v.optional(v.union(v.literal("comdirect"), v.literal("fints"))), lastError: v.optional(v.string()), }), v.null(), ), handler: async (ctx) => { const userId = await requireUserId(ctx); return await ctx.db .query("syncState") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); }, }); export const getPendingTan = query({ args: {}, returns: v.union( v.object({ _id: v.id("pendingTan"), status: v.union( v.literal("idle"), v.literal("awaiting"), v.literal("done"), v.literal("error"), ), challengeMessage: v.optional(v.string()), photoTanMimeType: v.optional(v.string()), photoTanBase64: v.optional(v.string()), isDecoupled: v.optional(v.boolean()), errorMessage: v.optional(v.string()), updatedAt: v.number(), }), v.null(), ), handler: async (ctx) => { const userId = await requireUserId(ctx); const pending = await ctx.db .query("pendingTan") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); if (!pending) return null; return { _id: pending._id, status: pending.status, challengeMessage: pending.challengeMessage, photoTanMimeType: pending.photoTanMimeType, photoTanBase64: pending.photoTanBase64, isDecoupled: pending.isDecoupled, errorMessage: pending.errorMessage, updatedAt: pending.updatedAt, }; }, }); export const updateConfig = mutation({ args: { providerPreference: v.optional( v.union(v.literal("auto"), v.literal("comdirect"), v.literal("fints")), ), fints: v.optional( v.object({ blz: v.string(), url: v.string(), login: v.string(), productId: v.string(), productVersion: v.optional(v.string()), tanMethodId: v.optional(v.number()), tanMediaName: v.optional(v.string()), }), ), }, returns: v.null(), handler: async (ctx, args) => { const userId = await requireUserId(ctx); const existing = await ctx.db .query("bankConfig") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); if (existing) { const patch: Record = {}; if (args.providerPreference !== undefined) { patch.providerPreference = args.providerPreference; } if (args.fints !== undefined) { patch.fints = { ...existing.fints, ...args.fints }; } if (Object.keys(patch).length > 0) { await ctx.db.patch(existing._id, patch); } } else { await ctx.db.insert("bankConfig", { userId, providerPreference: args.providerPreference ?? "auto", comdirectHasCredentials: false, fints: args.fints ?? { blz: "", url: "", login: "", productId: "", }, }); } return null; }, }); export const submitTan = mutation({ args: { tan: v.string() }, returns: v.null(), handler: async (ctx, args) => { const userId = await requireUserId(ctx); const tan = args.tan.trim(); if (!tan) throw new Error("Bitte TAN eingeben"); const pending = await ctx.db .query("pendingTan") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); if (!pending || pending.status !== "awaiting") { throw new Error("Keine laufende TAN-Anfrage"); } await ctx.db.patch(pending._id, { submittedTan: tan, updatedAt: Date.now(), }); return null; }, }); export const resetPendingTan = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { const userId = await requireUserId(ctx); const existing = await ctx.db .query("pendingTan") .withIndex("by_user", (q) => q.eq("userId", userId)) .unique(); if (existing) { await ctx.db.patch(existing._id, { status: "idle", challengeRef: undefined, challengeMessage: undefined, photoTanMimeType: undefined, photoTanBase64: undefined, errorMessage: undefined, syncJobJson: undefined, pollAttempt: undefined, isDecoupled: undefined, submittedTan: undefined, updatedAt: Date.now(), }); } return null; }, });