/// 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["./imports.test.ts"]; const commitRowsInternal = makeFunctionReference<"mutation">( "imports:commitRowsInternal", ); async function seedImportFixture() { const t = convexTest(schema, modules); const seeded = await t.run(async (ctx) => { const userId = await ctx.db.insert("users", { name: "Import User", email: "import@example.com", }); const accountId = await ctx.db.insert("accounts", { userId, name: "Girokonto", type: "giro", iban: "DE40200411110592825400", openingBalance: 0, currency: "EUR", isArchived: false, externalId: "592825400", }); return { userId, accountId }; }); return { t, seeded }; } describe("imports.commitRowsInternal", () => { test("imports multiple FinTS rows without external references when transaction content differs", async () => { const { t, seeded } = await seedImportFixture(); const result = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-2026-06-15-2026-06-23", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [ { accountId: seeded.accountId as Id<"accounts">, bookingDate: "2026-06-22", valueDate: "2026-06-22", description: "REWE", amount: -12.34, vorgang: "NMSC", isPending: false, rawText: "REWE SAGT DANKE", }, { accountId: seeded.accountId as Id<"accounts">, bookingDate: "2026-06-22", valueDate: "2026-06-22", description: "ALDI", amount: -23.45, vorgang: "NMSC", isPending: false, rawText: "ALDI SAGT DANKE", }, ], }); const rows = await t.run(async (ctx) => ctx.db .query("transactions") .withIndex("by_user_account", (q) => q.eq("userId", seeded.userId).eq("accountId", seeded.accountId), ) .collect(), ); expect(result.importedCount).toBe(2); expect(result.skippedCount).toBe(0); expect(rows.map((row) => row.description).sort()).toEqual(["ALDI", "REWE"]); expect(rows.every((row) => row.externalRef === undefined)).toBe(true); }); test("does not let a legacy positional external reference block new FinTS rows without external references", async () => { const { t, seeded } = await seedImportFixture(); await t.run(async (ctx) => { await ctx.db.insert("transactions", { userId: seeded.userId, accountId: seeded.accountId, bookingDate: "2024-09-30", valueDate: "2024-09-30", description: "Old positional transaction", amount: -9.99, isPending: false, effectiveMonth: "2024-09", dedupHash: "legacy-dedup", externalRef: "592825400:POS 13", }); }); const result = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-2026-06-15-2026-06-23", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [ { accountId: seeded.accountId as Id<"accounts">, bookingDate: "2026-06-22", valueDate: "2026-06-22", description: "HEM TANKSTELLE", amount: -62.01, vorgang: "NMSC", isPending: false, rawText: "HEM TANKSTELLE", }, ], }); expect(result.importedCount).toBe(1); expect(result.skippedCount).toBe(0); }); test("still skips real duplicate external references", async () => { const { t, seeded } = await seedImportFixture(); const row = { accountId: seeded.accountId as Id<"accounts">, bookingDate: "2026-06-22", valueDate: "2026-06-22", description: "Kartenzahlung", amount: -12.34, vorgang: "NMSC", isPending: false, rawText: "Kartenzahlung", externalRef: "592825400:5J2C21XL0470L56V/39761", }; const first = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-first", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [row], }); const second = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-second", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [{ ...row, description: "Different text same bank ref", amount: -99 }], }); expect(first.importedCount).toBe(1); expect(first.skippedCount).toBe(0); expect(second.importedCount).toBe(0); expect(second.skippedCount).toBe(1); }); test("skips repeated rows without external references via dedup hash", async () => { const { t, seeded } = await seedImportFixture(); const row = { accountId: seeded.accountId as Id<"accounts">, bookingDate: "2026-06-22", valueDate: "2026-06-22", description: "REWE", amount: -12.34, vorgang: "NMSC", isPending: false, rawText: "REWE SAGT DANKE", }; const first = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-first", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [row], }); const second = await t.mutation(commitRowsInternal, { userId: seeded.userId, filename: "fints-sync-second", source: "fints", accountId: seeded.accountId as Id<"accounts">, rows: [row], }); expect(first.importedCount).toBe(1); expect(first.skippedCount).toBe(0); expect(second.importedCount).toBe(0); expect(second.skippedCount).toBe(1); }); });