import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import request from "supertest"; import type { FastifyInstance } from "fastify"; import { buildApp } from "../src/server"; import { prisma, ensureUser, resetUser, U, closePrisma } from "./helpers"; let app: FastifyInstance; const CSRF = "test-csrf"; function mutate(path: string) { return request(app.server) .post(path) .set("x-user-id", U) .set("x-csrf-token", CSRF) .set("Cookie", [`csrf=${CSRF}`]); } beforeAll(async () => { app = await buildApp({ AUTH_DISABLED: true, SEED_DEFAULT_BUDGET: false }); await app.ready(); }); beforeEach(async () => { await resetUser(U); await ensureUser(U); await prisma.variableCategory.createMany({ data: [ { userId: U, name: "Essentials", percent: 60, priority: 10, balanceCents: 10_000n }, { userId: U, name: "Savings", percent: 40, priority: 20, balanceCents: 10_000n, isSavings: true }, ], }); }); afterAll(async () => { if (app) await app.close(); await closePrisma(); }); describe("estimated fixed plans true-up", () => { it("creates estimated mode plans with estimate fields", async () => { const dueOn = new Date("2026-04-01T00:00:00.000Z").toISOString(); const res = await mutate("/fixed-plans").send({ name: "Water", amountMode: "estimated", estimatedCents: 12000, totalCents: 12000, fundedCents: 0, priority: 10, dueOn, frequency: "monthly", autoPayEnabled: false, }); expect(res.status).toBe(201); const created = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: res.body.id } }); expect(created.amountMode).toBe("estimated"); expect(Number(created.estimatedCents ?? 0n)).toBe(12000); expect(Number(created.totalCents ?? 0n)).toBe(12000); }); it("true-up deficit auto-pulls from available budget and leaves remaining shortfall", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Water Deficit", amountMode: "estimated", estimatedCents: 10000n, totalCents: 10000n, fundedCents: 6000n, currentFundedCents: 6000n, priority: 10, cycleStart: new Date("2026-03-01T00:00:00.000Z"), dueOn: new Date("2026-04-01T00:00:00.000Z"), frequency: "monthly", }, }); const res = await mutate(`/fixed-plans/${plan.id}/true-up-actual`).send({ actualCents: 23000 }); expect(res.status).toBe(200); expect(res.body.deltaCents).toBe(13000); expect(res.body.autoPulledCents).toBe(13000); expect(res.body.remainingShortfallCents).toBe(4000); const updated = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id } }); expect(Number(updated.totalCents ?? 0n)).toBe(23000); expect(Number(updated.currentFundedCents ?? 0n)).toBe(19000); expect(Number(updated.actualCents ?? 0n)).toBe(23000); expect(updated.actualCycleDueOn?.toISOString()).toBe(updated.dueOn.toISOString()); expect(updated.actualRecordedAt).toBeTruthy(); }); it("true-up surplus refunds funded excess back to available budget", async () => { const categoriesBefore = await prisma.variableCategory.findMany({ where: { userId: U } }); const budgetBefore = categoriesBefore.reduce((sum, c) => sum + Number(c.balanceCents ?? 0n), 0); const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Water Surplus", amountMode: "estimated", estimatedCents: 15000n, totalCents: 15000n, fundedCents: 15000n, currentFundedCents: 15000n, priority: 10, cycleStart: new Date("2026-03-01T00:00:00.000Z"), dueOn: new Date("2026-04-01T00:00:00.000Z"), frequency: "monthly", }, }); const res = await mutate(`/fixed-plans/${plan.id}/true-up-actual`).send({ actualCents: 9000 }); expect(res.status).toBe(200); expect(res.body.deltaCents).toBe(-6000); expect(res.body.refundedCents).toBe(6000); const updated = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id } }); expect(Number(updated.totalCents ?? 0n)).toBe(9000); expect(Number(updated.currentFundedCents ?? 0n)).toBe(9000); const categoriesAfter = await prisma.variableCategory.findMany({ where: { userId: U } }); const budgetAfter = categoriesAfter.reduce((sum, c) => sum + Number(c.balanceCents ?? 0n), 0); expect(budgetAfter - budgetBefore).toBe(6000); }); it("rejects true-up for fixed mode plans", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Rent Fixed", amountMode: "fixed", totalCents: 120000n, fundedCents: 120000n, currentFundedCents: 120000n, priority: 10, cycleStart: new Date("2026-03-01T00:00:00.000Z"), dueOn: new Date("2026-04-01T00:00:00.000Z"), frequency: "monthly", }, }); const res = await mutate(`/fixed-plans/${plan.id}/true-up-actual`).send({ actualCents: 100000 }); expect(res.status).toBe(400); expect(res.body.code).toBe("PLAN_NOT_ESTIMATED"); }); it("pay-now rollover resets estimated plan target back to estimate and clears cycle actual metadata", async () => { const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Water Rollover", amountMode: "estimated", estimatedCents: 10000n, totalCents: 16000n, fundedCents: 16000n, currentFundedCents: 16000n, actualCents: 16000n, actualCycleDueOn: new Date("2026-04-01T00:00:00.000Z"), actualRecordedAt: new Date(), priority: 10, cycleStart: new Date("2026-03-01T00:00:00.000Z"), dueOn: new Date("2026-04-01T00:00:00.000Z"), frequency: "monthly", }, }); const res = await mutate(`/fixed-plans/${plan.id}/pay-now`).send({}); expect(res.status).toBe(200); const updated = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id } }); expect(Number(updated.totalCents ?? 0n)).toBe(10000); expect(updated.actualCents).toBeNull(); expect(updated.actualCycleDueOn).toBeNull(); expect(updated.actualRecordedAt).toBeNull(); }); });