feat: add guarded savings agent tools
This commit is contained in:
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user