import request from "supertest"; import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import appFactory from "./appFactory"; import { prisma, resetUser, ensureUser, U, cid, closePrisma } from "./helpers"; import type { FastifyInstance } from "fastify"; let app: FastifyInstance; beforeAll(async () => { app = await appFactory(); }); afterAll(async () => { if (app) await app.close(); await closePrisma(); }); async function seedBasics() { await resetUser(U); await ensureUser(U); await prisma.variableCategory.createMany({ data: [ { id: cid("s"), userId: U, name: "savings", percent: 30, priority: 1, isSavings: true, balanceCents: 3000n }, { id: cid("f"), userId: U, name: "food", percent: 20, priority: 2, isSavings: false, balanceCents: 2000n }, { id: cid("g"), userId: U, name: "gas", percent: 30, priority: 3, isSavings: false, balanceCents: 3000n }, { id: cid("m"), userId: U, name: "misc", percent: 20, priority: 4, isSavings: false, balanceCents: 2000n }, ], }); await prisma.budgetSession.create({ data: { userId: U, periodStart: new Date("2026-03-01T00:00:00Z"), periodEnd: new Date("2026-04-01T00:00:00Z"), totalBudgetCents: 10_000n, allocatedCents: 0n, fundedCents: 0n, availableCents: 10_000n, }, }); } describe("manual rebalance", () => { beforeEach(async () => { await seedBasics(); }); it("rebalances when sums match available", async () => { const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); const targets = cats.map((c) => ({ id: c.id, targetCents: 2500 })); // 4 * 2500 = 10000 const res = await request(app.server) .post("/variable-categories/manual-rebalance") .set("x-user-id", U) .send({ targets }); expect(res.statusCode).toBe(200); expect(res.body?.ok).toBe(true); const updated = await prisma.variableCategory.findMany({ where: { userId: U } }); expect(updated.every((c) => Number(c.balanceCents) === 2500)).toBe(true); }); it("rejects sum mismatch", async () => { const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); const targets = cats.map((c, i) => ({ id: c.id, targetCents: i === 0 ? 5000 : 1000 })); // 5000 + 3*1000 = 8000 const res = await request(app.server) .post("/variable-categories/manual-rebalance") .set("x-user-id", U) .send({ targets }); expect(res.statusCode).toBe(400); expect(res.body?.code).toBe("SUM_MISMATCH"); }); it("requires savings confirmation when lowering below floor", async () => { const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); // savings to 500 (below 20% of 10000 = 2000) const targets = cats.map((c) => ({ id: c.id, targetCents: c.isSavings ? 500 : 3166 })); // 500 + 3*3166 = 9998 -> adjust targets[1].targetCents += 2; // total 10000 const res = await request(app.server) .post("/variable-categories/manual-rebalance") .set("x-user-id", U) .send({ targets }); expect(res.statusCode).toBe(400); expect(res.body?.code).toBe("SAVINGS_FLOOR"); const resOk = await request(app.server) .post("/variable-categories/manual-rebalance") .set("x-user-id", U) .send({ targets, forceLowerSavings: true }); expect(resOk.statusCode).toBe(200); }); it("blocks >80% single category", async () => { const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); const targets = cats.map((c, i) => ({ id: c.id, targetCents: i === 0 ? 9000 : 333 })); targets[1].targetCents += 1; // sum 10000 const res = await request(app.server) .post("/variable-categories/manual-rebalance") .set("x-user-id", U) .send({ targets }); expect(res.statusCode).toBe(400); expect(res.body?.code).toBe("OVER_80_PERCENT"); }); });