import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import request from "supertest"; import appFactory from "./appFactory"; import { prisma, ensureUser, resetUser, U, closePrisma } from "./helpers"; import type { FastifyInstance } from "fastify"; let app: FastifyInstance; beforeAll(async () => { app = await appFactory(); }); beforeEach(async () => { await resetUser(U); await ensureUser(U); }); afterAll(async () => { await app.close(); await closePrisma(); }); describe("Payment-Triggered Rollover", () => { it("advances due date for weekly frequency on payment", async () => { // Create a fixed plan with weekly frequency const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Weekly Subscription", totalCents: 1000n, fundedCents: 1000n, currentFundedCents: 1000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: new Date("2025-12-01T00:00:00Z"), frequency: "weekly", }, }); // Make payment const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 1000, planId: plan.id, }); if (txRes.status !== 200) { console.log("Response status:", txRes.status); console.log("Response body:", txRes.body); } expect(txRes.status).toBe(200); // Check plan was updated with next due date (7 days later) const updated = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(updated?.fundedCents).toBe(0n); expect(updated?.currentFundedCents).toBe(0n); expect(updated?.dueOn.toISOString()).toBe("2025-12-08T00:00:00.000Z"); }); it("advances due date for biweekly frequency on payment", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Biweekly Bill", totalCents: 5000n, fundedCents: 5000n, currentFundedCents: 5000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: new Date("2025-12-01T00:00:00Z"), frequency: "biweekly", }, }); const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 5000, planId: plan.id, }); expect(txRes.status).toBe(200); const updated = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(updated?.fundedCents).toBe(0n); expect(updated?.dueOn.toISOString()).toBe("2025-12-15T00:00:00.000Z"); }); it("advances due date for monthly frequency on payment", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Monthly Rent", totalCents: 100000n, fundedCents: 100000n, currentFundedCents: 100000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: new Date("2025-12-01T00:00:00Z"), frequency: "monthly", }, }); const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 100000, planId: plan.id, }); expect(txRes.status).toBe(200); const updated = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(updated?.fundedCents).toBe(0n); expect(updated?.dueOn.toISOString()).toBe("2026-01-01T00:00:00.000Z"); }); it("does not advance due date for one-time frequency", async () => { const originalDueDate = new Date("2025-12-01T00:00:00Z"); const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "One-time Expense", totalCents: 2000n, fundedCents: 2000n, currentFundedCents: 2000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: originalDueDate, frequency: "one-time", }, }); const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 2000, planId: plan.id, }); expect(txRes.status).toBe(200); const updated = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(updated?.fundedCents).toBe(0n); // Due date should remain unchanged for one-time expenses expect(updated?.dueOn.toISOString()).toBe(originalDueDate.toISOString()); }); it("does not advance due date when no frequency is set", async () => { const originalDueDate = new Date("2025-12-01T00:00:00Z"); const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Manual Bill", totalCents: 3000n, fundedCents: 3000n, currentFundedCents: 3000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: originalDueDate, frequency: null, }, }); const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 3000, planId: plan.id, }); expect(txRes.status).toBe(200); const updated = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(updated?.fundedCents).toBe(0n); // Due date should remain unchanged when no frequency expect(updated?.dueOn.toISOString()).toBe(originalDueDate.toISOString()); }); it("prevents payment when insufficient funded amount", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Underfunded Bill", totalCents: 10000n, fundedCents: 5000n, currentFundedCents: 5000n, priority: 10, cycleStart: new Date("2025-11-01T00:00:00Z"), dueOn: new Date("2025-12-01T00:00:00Z"), frequency: "monthly", }, }); // Try to pay more than funded amount const txRes = await request(app.server) .post("/transactions") .set("x-user-id", U) .send({ occurredAtISO: "2025-11-27T12:00:00Z", kind: "fixed_payment", amountCents: 10000, planId: plan.id, }); expect(txRes.status).toBe(400); expect(txRes.body.code).toBe("OVERDRAFT_PLAN"); // Plan should remain unchanged const unchanged = await prisma.fixedPlan.findUnique({ where: { id: plan.id } }); expect(unchanged?.fundedCents).toBe(5000n); expect(unchanged?.dueOn.toISOString()).toBe("2025-12-01T00:00:00.000Z"); }); });