142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
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("uses live category balances when session availableCents is stale", async () => {
|
|
await prisma.budgetSession.updateMany({
|
|
where: { userId: U },
|
|
data: { availableCents: 1234n },
|
|
});
|
|
|
|
const getRes = await request(app.server)
|
|
.get("/variable-categories/manual-rebalance")
|
|
.set("x-user-id", U);
|
|
|
|
expect(getRes.statusCode).toBe(200);
|
|
expect(getRes.body?.availableCents).toBe(10_000);
|
|
|
|
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 postRes = await request(app.server)
|
|
.post("/variable-categories/manual-rebalance")
|
|
.set("x-user-id", U)
|
|
.send({ targets });
|
|
|
|
expect(postRes.statusCode).toBe(200);
|
|
expect(postRes.body?.availableCents).toBe(10_000);
|
|
|
|
const session = await prisma.budgetSession.findFirst({
|
|
where: { userId: U },
|
|
orderBy: { periodStart: "desc" },
|
|
select: { availableCents: true },
|
|
});
|
|
expect(Number(session?.availableCents ?? 0n)).toBe(10_000);
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|