feat: add guarded savings agent tools

This commit is contained in:
Matthias
2026-06-23 21:13:02 +02:00
parent 6e687dc9b5
commit b7dae31fe1
22 changed files with 3510 additions and 73 deletions

View File

@@ -33,6 +33,77 @@ type ComdirectProviderContext = {
userId: Id<"users">;
};
type ComdirectBalancePayload = {
values?: Array<Record<string, unknown>>;
};
function getNestedText(value: unknown): string | undefined {
if (!value || typeof value !== "object") return undefined;
const text = (value as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
}
export function mapComdirectBalances(
payload: ComdirectBalancePayload,
): NormalizedBalance[] {
return (payload.values ?? []).flatMap((item) => {
const account = item.account as
| {
accountId?: unknown;
}
| undefined;
const balance = item.balance as
| {
value?: unknown;
unit?: unknown;
}
| undefined;
const externalId =
typeof account?.accountId === "string" ? account.accountId : undefined;
if (!externalId) return [];
const balanceValue = Number(balance?.value ?? 0);
const currency = typeof balance?.unit === "string" ? balance.unit : "EUR";
const asOf = typeof item.date === "string" ? item.date : undefined;
return [
{
externalId,
balance: Number.isFinite(balanceValue) ? balanceValue : 0,
currency,
asOf,
},
];
});
}
export function mapComdirectAccounts(
payload: ComdirectBalancePayload,
): NormalizedAccount[] {
return (payload.values ?? []).flatMap((item) => {
const account = item.account as
| {
accountId?: unknown;
iban?: unknown;
accountType?: unknown;
}
| undefined;
const accountIdExternal =
typeof account?.accountId === "string" ? account.accountId : undefined;
if (!accountIdExternal) return [];
const balance = mapComdirectBalances({ values: [item] })[0];
return [
{
externalId: accountIdExternal,
name: getNestedText(account?.accountType) ?? "comdirect Konto",
iban: typeof account?.iban === "string" ? account.iban : undefined,
balance: balance?.balance ?? 0,
currency: balance?.currency ?? "EUR",
},
];
});
}
export async function createComdirectRestProvider(
context: ComdirectProviderContext,
): Promise<BankDataProvider> {
@@ -57,39 +128,16 @@ export async function createComdirectRestProvider(
async getAccounts(): Promise<NormalizedAccount[]> {
const balances = await getAccountBalances(accessToken, sessionUuid);
return (balances.values ?? []).flatMap((item) => {
const account = item.account as {
accountId?: string;
iban?: string;
accountType?: { text?: string };
};
const accountIdExternal = account?.accountId;
if (!accountIdExternal) return [];
const balanceValue = Number((item.balance as { value?: string })?.value ?? 0);
return [
{
externalId: accountIdExternal,
name: account.accountType?.text ?? "comdirect Konto",
iban: account.iban,
balance: balanceValue,
currency: "EUR",
},
];
});
return mapComdirectAccounts(balances);
},
async getBalance(accountExternalId: string): Promise<NormalizedBalance> {
const balances = await getAccountBalances(accessToken, sessionUuid);
const match = (balances.values ?? []).find((item) => {
const account = item.account as { accountId?: string };
return account?.accountId === accountExternalId;
});
const match = mapComdirectBalances(balances).find(
(item) => item.externalId === accountExternalId,
);
if (!match) throw new Error(`Konto ${accountExternalId} nicht gefunden`);
return {
externalId: accountExternalId,
balance: Number((match.balance as { value?: string })?.value ?? 0),
currency: "EUR",
};
return match;
},
async getTransactions(
@@ -100,7 +148,7 @@ export async function createComdirectRestProvider(
const rows: NormalizedTransaction[] = [];
for (const state of ["BOOKED", "NOTBOOKED"] as const) {
let offset = 0;
let matches = 0;
let matches: number;
do {
const result = await getTransactions(accessToken, sessionUuid, accountExternalId, {
transactionState: state,
@@ -207,3 +255,53 @@ export async function fetchComdirectData(
return { accounts, transactionsByAccount };
}
export async function fetchComdirectBalanceData(
ctx: ActionCtx,
userId: Id<"users">,
filterAccountId: Id<"accounts"> | undefined,
): Promise<{
accounts: NormalizedAccount[];
balances: NormalizedBalance[];
}> {
const clientId = process.env.COMDIRECT_CLIENT_ID;
const clientSecret = process.env.COMDIRECT_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error("comdirect API-Zugangsdaten nicht konfiguriert");
}
const session = await ctx.runQuery(internal.comdirect.internal.getSession, { userId });
if (!session?.accessToken || !session.secondaryActive) {
throw new Error("comdirect-Session nicht aktiv. Bitte erneut anmelden.");
}
const payload = await getAccountBalances(session.accessToken, session.sessionUuid);
const accounts = mapComdirectAccounts(payload);
const balances = mapComdirectBalances(payload);
if (!filterAccountId) {
return { accounts, balances };
}
const filteredAccounts: NormalizedAccount[] = [];
const allowedExternalIds = new Set<string>();
for (const account of accounts) {
const convexId = await ctx.runMutation(internal.bank.internal.upsertAccountFromProvider, {
userId,
externalId: account.externalId,
name: account.name,
iban: account.iban,
balance: account.balance,
currency: account.currency,
});
if (convexId === filterAccountId) {
filteredAccounts.push(account);
allowedExternalIds.add(account.externalId);
}
}
return {
accounts: filteredAccounts,
balances: balances.filter((balance) => allowedExternalIds.has(balance.externalId)),
};
}