Files
SkyMoney/api/tests/fixed-plans.estimated-true-up.test.ts
Ricearoni1245 301b3f8967
All checks were successful
Deploy / deploy (push) Successful in 1m26s
Security Tests / security-non-db (push) Successful in 20s
Security Tests / security-db (push) Successful in 22s
feat: added estimate fixed expenses
2026-03-02 10:49:12 -06:00

180 lines
6.2 KiB
TypeScript

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();
});
});