Add bank synchronization features with FinTS support and update dependencies
This commit is contained in:
209
convex/bank/config.ts
Normal file
209
convex/bank/config.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user