import request from "supertest"; import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest"; import appFactory from "./appFactory"; import { prisma, resetUser, ensureUser, U, cid, pid, closePrisma } from "./helpers"; import type { FastifyInstance } from "fastify"; let app: FastifyInstance; beforeAll(async () => { app = await appFactory(); }); afterAll(async () => { await app.close(); await closePrisma(); }); describe("GET /transactions", () => { let catId: string; let planId: string; beforeEach(async () => { await resetUser(U); await ensureUser(U); catId = cid("c"); planId = pid("p"); await prisma.variableCategory.create({ data: { id: catId, userId: U, name: "Groceries", percent: 100, priority: 1, isSavings: false, balanceCents: 5000n }, }); await prisma.fixedPlan.create({ data: { id: planId, userId: U, name: "Rent", totalCents: 10000n, fundedCents: 2000n, priority: 1, cycleStart: new Date().toISOString(), dueOn: new Date(Date.now() + 864e5).toISOString(), fundingMode: "auto-on-deposit", }, }); await prisma.transaction.createMany({ data: [ { id: `t_${Date.now()}_1`, userId: U, occurredAt: new Date("2025-01-03T12:00:00.000Z"), kind: "variable_spend", categoryId: catId, amountCents: 1000n, }, { id: `t_${Date.now()}_2`, userId: U, occurredAt: new Date("2025-01-10T12:00:00.000Z"), kind: "fixed_payment", planId, amountCents: 2000n, }, ], }); }); it("paginates + filters by kind/date", async () => { const res = await request(app.server) .get("/transactions?from=2025-01-02&to=2025-01-06&kind=variable_spend&page=1&limit=10") .set("x-user-id", U); expect(res.statusCode).toBe(200); const body = res.body; expect(Array.isArray(body.items)).toBe(true); expect(body.items.length).toBe(1); expect(body.items[0].kind).toBe("variable_spend"); }); it("filters by bucket id for either category or plan", async () => { const byCategory = await request(app.server) .get(`/transactions?bucketId=${catId}`) .set("x-user-id", U); expect(byCategory.status).toBe(200); expect(byCategory.body.items.every((t: any) => t.categoryId === catId)).toBe(true); const byPlan = await request(app.server) .get(`/transactions?bucketId=${planId}`) .set("x-user-id", U); expect(byPlan.status).toBe(200); expect(byPlan.body.items.every((t: any) => t.planId === planId)).toBe(true); }); }); describe("POST /transactions", () => { const dateISO = new Date().toISOString(); let catId: string; let planId: string; beforeEach(async () => { await resetUser(U); await ensureUser(U); catId = cid("cat"); planId = pid("plan"); await prisma.variableCategory.create({ data: { id: catId, userId: U, name: "Dining", percent: 100, priority: 1, isSavings: false, balanceCents: 5000n, }, }); await prisma.fixedPlan.create({ data: { id: planId, userId: U, name: "Loan", totalCents: 10000n, fundedCents: 3000n, priority: 1, cycleStart: new Date().toISOString(), dueOn: new Date(Date.now() + 864e5).toISOString(), fundingMode: "auto-on-deposit", }, }); }); it("spends from a variable category and updates balance", async () => { const res = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ kind: "variable_spend", amountCents: 2000, occurredAtISO: dateISO, categoryId: catId, note: "Groceries run", receiptUrl: "https://example.com/receipt", isReconciled: true, }); expect(res.status).toBe(200); const category = await prisma.variableCategory.findUniqueOrThrow({ where: { id: catId } }); expect(Number(category.balanceCents)).toBe(3000); const tx = await prisma.transaction.findFirstOrThrow({ where: { userId: U, categoryId: catId } }); expect(tx.note).toBe("Groceries run"); expect(tx.receiptUrl).toBe("https://example.com/receipt"); expect(tx.isReconciled).toBe(true); }); it("prevents overdrawing fixed plans", async () => { const res = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ kind: "fixed_payment", amountCents: 400000, // exceeds funded occurredAtISO: dateISO, planId, }); expect(res.status).toBe(400); expect(res.body.code).toBe("OVERDRAFT_PLAN"); }); it("updates note/receipt and reconciliation via patch", async () => { const created = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ kind: "variable_spend", amountCents: 1000, occurredAtISO: dateISO, categoryId: catId, }); expect(created.status).toBe(200); const txId = created.body.id; const res = await request(app.server) .patch(`/transactions/${txId}`) .set("x-user-id", U) .send({ note: "Cleared", isReconciled: true, receiptUrl: "https://example.com/r.pdf", }); expect(res.status).toBe(200); expect(res.body.isReconciled).toBe(true); expect(res.body.note).toBe("Cleared"); expect(res.body.receiptUrl).toBe("https://example.com/r.pdf"); const tx = await prisma.transaction.findUniqueOrThrow({ where: { id: txId } }); expect(tx.isReconciled).toBe(true); }); });