Add savings chat analysis feature
This commit is contained in:
194
convex/savingsChat.test.ts
Normal file
194
convex/savingsChat.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { convexTest } from "convex-test";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import schema from "./schema";
|
||||
|
||||
const modules = import.meta.glob("./**/*.ts");
|
||||
delete modules["./savingsChat.test.ts"];
|
||||
|
||||
describe("savingsChat.getContext", () => {
|
||||
test("counts and sums every matching transaction before applying prompt limits", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
|
||||
const seeded = await t.run(async (ctx) => {
|
||||
const userId = await ctx.db.insert("users", {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
});
|
||||
const giroAccountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Girokonto",
|
||||
type: "checking",
|
||||
openingBalance: 0,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
});
|
||||
const otherAccountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Tagesgeld",
|
||||
type: "savings",
|
||||
openingBalance: 0,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
});
|
||||
|
||||
const amounts: number[] = [];
|
||||
const months = ["2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"];
|
||||
for (let index = 0; index < 450; index++) {
|
||||
const month = months[index % months.length];
|
||||
const day = String((index % 27) + 1).padStart(2, "0");
|
||||
const bookingDate = `${month}-${day}`;
|
||||
const amount = index % 3 === 0 ? 100 : -25;
|
||||
amounts.push(amount);
|
||||
await ctx.db.insert("transactions", {
|
||||
userId,
|
||||
accountId: giroAccountId,
|
||||
bookingDate,
|
||||
valueDate: bookingDate,
|
||||
description: `Giro transaction ${index}`,
|
||||
counterparty: "Counterparty",
|
||||
amount,
|
||||
isPending: false,
|
||||
effectiveMonth: index % 10 === 0 ? undefined : bookingDate.slice(0, 7),
|
||||
});
|
||||
}
|
||||
|
||||
for (let index = 0; index < 50; index++) {
|
||||
const bookingDate = `2026-06-${String((index % 27) + 1).padStart(2, "0")}`;
|
||||
await ctx.db.insert("transactions", {
|
||||
userId,
|
||||
accountId: otherAccountId,
|
||||
bookingDate,
|
||||
valueDate: bookingDate,
|
||||
description: `Other account transaction ${index}`,
|
||||
amount: 999,
|
||||
isPending: false,
|
||||
effectiveMonth: bookingDate.slice(0, 7),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userId,
|
||||
giroAccountId,
|
||||
expectedIncome: amounts.filter((amount) => amount > 0).reduce((sum, amount) => sum + amount, 0),
|
||||
expectedExpenses: amounts.filter((amount) => amount < 0).reduce((sum, amount) => sum + amount, 0),
|
||||
expectedBalance: amounts.reduce((sum, amount) => sum + amount, 0),
|
||||
};
|
||||
});
|
||||
|
||||
const asUser = t.withIdentity({
|
||||
subject: `${seeded.userId}|test-session`,
|
||||
tokenIdentifier: `test:${seeded.userId}`,
|
||||
});
|
||||
|
||||
const context = await asUser.query(api.savingsChat.getContext, {
|
||||
from: "2025-12-01",
|
||||
to: "2026-06-30",
|
||||
accountId: seeded.giroAccountId as Id<"accounts">,
|
||||
basis: "effective",
|
||||
});
|
||||
|
||||
expect(context.accountName).toBe("Girokonto");
|
||||
expect(context.isComplete).toBe(true);
|
||||
expect(context.totals.transactionCount).toBe(450);
|
||||
expect(context.totals.income).toBe(seeded.expectedIncome);
|
||||
expect(context.totals.expenses).toBe(seeded.expectedExpenses);
|
||||
expect(context.totals.balance).toBe(seeded.expectedBalance);
|
||||
});
|
||||
|
||||
test("builds complete prompt lines for every matching transaction", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
|
||||
const seeded = await t.run(async (ctx) => {
|
||||
const userId = await ctx.db.insert("users", {
|
||||
name: "Prompt User",
|
||||
email: "prompt@example.com",
|
||||
});
|
||||
const categoryId = await ctx.db.insert("categories", {
|
||||
userId,
|
||||
name: "Lebensmittel",
|
||||
kind: "ausgabe",
|
||||
block: "variabel",
|
||||
color: "#22c55e",
|
||||
sortOrder: 1,
|
||||
isSystem: false,
|
||||
});
|
||||
const giroAccountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Girokonto",
|
||||
type: "checking",
|
||||
openingBalance: 0,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
});
|
||||
const otherAccountId = await ctx.db.insert("accounts", {
|
||||
userId,
|
||||
name: "Depot",
|
||||
type: "investment",
|
||||
openingBalance: 0,
|
||||
currency: "EUR",
|
||||
isArchived: false,
|
||||
});
|
||||
|
||||
await ctx.db.insert("transactions", {
|
||||
userId,
|
||||
accountId: giroAccountId,
|
||||
categoryId,
|
||||
bookingDate: "2026-02-14",
|
||||
valueDate: "2026-02-14",
|
||||
description: "Supermarkt",
|
||||
counterparty: "Markt GmbH",
|
||||
amount: -42.5,
|
||||
isPending: false,
|
||||
});
|
||||
await ctx.db.insert("transactions", {
|
||||
userId,
|
||||
accountId: giroAccountId,
|
||||
bookingDate: "2026-02-15",
|
||||
valueDate: "2026-02-15",
|
||||
description: "Gehalt",
|
||||
counterparty: "Arbeitgeber",
|
||||
amount: 2500,
|
||||
isPending: false,
|
||||
effectiveMonth: "2026-02",
|
||||
});
|
||||
await ctx.db.insert("transactions", {
|
||||
userId,
|
||||
accountId: otherAccountId,
|
||||
bookingDate: "2026-02-16",
|
||||
valueDate: "2026-02-16",
|
||||
description: "Other account should not appear",
|
||||
amount: 999,
|
||||
isPending: false,
|
||||
effectiveMonth: "2026-02",
|
||||
});
|
||||
|
||||
return { userId, giroAccountId };
|
||||
});
|
||||
|
||||
const asUser = t.withIdentity({
|
||||
subject: `${seeded.userId}|test-session`,
|
||||
tokenIdentifier: `test:${seeded.userId}`,
|
||||
});
|
||||
|
||||
const context = await asUser.query(internal.savingsChat.getPromptContext, {
|
||||
from: "2026-02-01",
|
||||
to: "2026-02-28",
|
||||
accountId: seeded.giroAccountId as Id<"accounts">,
|
||||
basis: "effective",
|
||||
});
|
||||
|
||||
expect(context.totals.transactionCount).toBe(2);
|
||||
expect(context.transactionLines).toHaveLength(2);
|
||||
expect(context.transactionLines.join("\n")).toContain(
|
||||
"2026-02-14 | Supermarkt (Markt GmbH) | -42.50€ | Lebensmittel | Girokonto",
|
||||
);
|
||||
expect(context.transactionLines.join("\n")).toContain(
|
||||
"2026-02-15 | Gehalt (Arbeitgeber) | 2500.00€ | Ohne Kategorie | Girokonto",
|
||||
);
|
||||
expect(context.transactionLines.join("\n")).not.toContain("Other account should not appear");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user