132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
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<Id<"accounts">, 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,
|
|
});
|
|
},
|
|
});
|