179 lines
5.0 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|