Files
finanzen/convex/bank/config.ts

210 lines
5.5 KiB
TypeScript

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<string, unknown> = {};
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;
},
});