Files
finanzen/convex/balances.test.ts
2026-06-23 21:13:02 +02:00

179 lines
5.0 KiB
TypeScript

/// <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",
});
});
});