feat: add guarded savings agent tools
This commit is contained in:
178
convex/balances.test.ts
Normal file
178
convex/balances.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { convexTest } from "convex-test";
|
||||
import { makeFunctionReference } from "convex/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import schema from "./schema";
|
||||
|
||||
const modules = import.meta.glob("./**/*.ts");
|
||||
delete modules["./balances.test.ts"];
|
||||
|
||||
const listLatest = makeFunctionReference<"query">("bank/balances:listLatest");
|
||||
const upsertAccountBalance = makeFunctionReference<"mutation">(
|
||||
"bank/internal:upsertAccountBalance",
|
||||
);
|
||||
|
||||
type LatestBalanceRow = {
|
||||
accountId: Id<"accounts">;
|
||||
accountName: string;
|
||||
balance: number | null;
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
async function seedBalanceFixture() {
|
||||
const t = convexTest(schema, modules);
|
||||
|
||||
const seeded = await t.run(async (ctx) => {
|
||||
const userId = await ctx.db.insert("users", {
|
||||
name: "Balance User",
|
||||
email: "balance@example.com",
|
||||
});
|
||||
const otherUserId = await ctx.db.insert("users", {
|
||||
name: "Other User",
|
||||
email: "other-balance@example.com",
|
||||
});
|
||||
const accountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Girokonto",
|
||||
type: "giro",
|
||||
iban: "DE89370400440532013000",
|
||||
openingBalance: 125,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
externalId: "giro-1",
|
||||
});
|
||||
const archivedAccountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Altes Konto",
|
||||
type: "giro",
|
||||
openingBalance: 50,
|
||||
currency: "EUR",
|
||||
isArchived: true,
|
||||
externalId: "old-1",
|
||||
});
|
||||
const otherAccountId = await ctx.db.insert("accounts", {
|
||||
userId: otherUserId,
|
||||
name: "Hidden Konto",
|
||||
type: "giro",
|
||||
openingBalance: 999,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
externalId: "hidden-1",
|
||||
});
|
||||
|
||||
return { userId, accountId, archivedAccountId, otherUserId, otherAccountId };
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
seeded,
|
||||
asUser: t.withIdentity({
|
||||
subject: `${seeded.userId}|test-session`,
|
||||
tokenIdentifier: `test:${seeded.userId}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("bank/balances.listLatest", () => {
|
||||
test("returns a missing state for active accounts without live balance snapshots", async () => {
|
||||
const { asUser, seeded } = await seedBalanceFixture();
|
||||
|
||||
const result = await asUser.query(listLatest, {});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
accountId: seeded.accountId,
|
||||
accountName: "Girokonto",
|
||||
accountType: "giro",
|
||||
iban: "DE89370400440532013000",
|
||||
externalId: "giro-1",
|
||||
balance: null,
|
||||
currency: "EUR",
|
||||
provider: null,
|
||||
fetchedAt: null,
|
||||
status: "missing",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns the newest live balance without mutating openingBalance", async () => {
|
||||
const { t, asUser, seeded } = await seedBalanceFixture();
|
||||
|
||||
await t.mutation(upsertAccountBalance, {
|
||||
userId: seeded.userId,
|
||||
accountId: seeded.accountId as Id<"accounts">,
|
||||
externalId: "giro-1",
|
||||
provider: "fints",
|
||||
balance: 900,
|
||||
currency: "EUR",
|
||||
fetchedAt: 1_780_000_000_000,
|
||||
status: "fresh",
|
||||
});
|
||||
await t.mutation(upsertAccountBalance, {
|
||||
userId: seeded.userId,
|
||||
accountId: seeded.accountId as Id<"accounts">,
|
||||
externalId: "giro-1",
|
||||
provider: "comdirect",
|
||||
balance: 1234.56,
|
||||
currency: "EUR",
|
||||
asOf: "2026-06-23",
|
||||
fetchedAt: 1_780_000_060_000,
|
||||
status: "fresh",
|
||||
});
|
||||
|
||||
const result = await asUser.query(listLatest, {});
|
||||
const account = await t.run((ctx) => ctx.db.get(seeded.accountId));
|
||||
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
accountId: seeded.accountId,
|
||||
accountName: "Girokonto",
|
||||
balance: 1234.56,
|
||||
currency: "EUR",
|
||||
provider: "comdirect",
|
||||
fetchedAt: 1_780_000_060_000,
|
||||
asOf: "2026-06-23",
|
||||
status: "fresh",
|
||||
},
|
||||
]);
|
||||
expect(account?.openingBalance).toBe(125);
|
||||
});
|
||||
|
||||
test("can include archived accounts for settings and preserves error snapshots", async () => {
|
||||
const { t, asUser, seeded } = await seedBalanceFixture();
|
||||
|
||||
await t.mutation(upsertAccountBalance, {
|
||||
userId: seeded.userId,
|
||||
accountId: seeded.archivedAccountId as Id<"accounts">,
|
||||
externalId: "old-1",
|
||||
provider: "fints",
|
||||
balance: 50,
|
||||
currency: "EUR",
|
||||
fetchedAt: 1_780_000_060_000,
|
||||
status: "error",
|
||||
errorMessage: "HKSAL wird nicht unterstuetzt",
|
||||
});
|
||||
|
||||
const defaultResult = await asUser.query(listLatest, {});
|
||||
const settingsResult = await asUser.query(listLatest, { includeArchived: true });
|
||||
|
||||
expect(defaultResult.map((row: LatestBalanceRow) => row.accountId)).toEqual([
|
||||
seeded.accountId,
|
||||
]);
|
||||
expect(settingsResult).toHaveLength(2);
|
||||
expect(
|
||||
settingsResult.find(
|
||||
(row: LatestBalanceRow) => row.accountId === seeded.archivedAccountId,
|
||||
),
|
||||
)
|
||||
.toMatchObject({
|
||||
accountName: "Altes Konto",
|
||||
balance: 50,
|
||||
status: "error",
|
||||
errorMessage: "HKSAL wird nicht unterstuetzt",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user