Files
SkyMoney/api/tests/budget-allocation.test.ts

132 lines
4.8 KiB
TypeScript

// tests/budget-allocation.test.ts
import { describe, it, expect, beforeEach, afterAll } from "vitest";
import { prisma, resetUser, ensureUser, U, cid, pid, closePrisma } from "./helpers";
import { allocateBudget } from "../src/allocator";
describe("Budget Allocation for Irregular Income", () => {
beforeEach(async () => {
await resetUser(U);
await ensureUser(U);
// Update user to irregular income type
await prisma.user.update({
where: { id: U },
data: {
incomeType: "irregular",
totalBudgetCents: 300000n, // $3000 monthly budget
budgetPeriod: "monthly",
},
});
});
afterAll(async () => {
await closePrisma();
});
describe("allocateBudget", () => {
it("allocates budget between fixed plans and variable categories", async () => {
const c1 = cid("groceries");
const c2 = cid("savings");
await prisma.variableCategory.createMany({
data: [
{ id: c1, userId: U, name: "Groceries", percent: 60, priority: 2, isSavings: false, balanceCents: 0n },
{ id: c2, userId: U, name: "Savings", percent: 40, priority: 1, isSavings: true, balanceCents: 0n },
],
});
const p1 = pid("rent");
await prisma.fixedPlan.create({
data: {
id: p1,
userId: U,
name: "Rent",
cycleStart: new Date().toISOString(),
dueOn: new Date(Date.now() + 21 * 86400000).toISOString(), // due in 3 weeks
totalCents: 120000n, // $1200 rent
fundedCents: 0n,
currentFundedCents: 0n,
priority: 1,
fundingMode: "auto-on-deposit",
},
});
// Allocate $3000 budget with 30% to fixed expenses
const result = await allocateBudget(prisma as any, U, 300000, 30);
expect(result).toBeDefined();
expect(result.totalBudgetCents).toBe(300000);
// Should have both fixed and variable allocations
expect(result.fixedAllocations.length).toBeGreaterThan(0);
expect(result.variableAllocations.length).toBeGreaterThan(0);
// Total allocation should not exceed budget
const totalAllocated = result.fundedBudgetCents + result.availableBudgetCents;
expect(totalAllocated).toBeLessThanOrEqual(300000);
// Rent should get some funding
const rentAllocation = result.fixedAllocations.find(a => a.fixedPlanId === p1);
expect(rentAllocation).toBeDefined();
expect(rentAllocation!.amountCents).toBeGreaterThan(0);
});
it("handles crisis mode with longer window for irregular income", async () => {
const c1 = cid("emergency");
await prisma.variableCategory.create({
data: { id: c1, userId: U, name: "Emergency", percent: 100, priority: 1, isSavings: true, balanceCents: 0n },
});
const p1 = pid("urgent");
await prisma.fixedPlan.create({
data: {
id: p1,
userId: U,
name: "Urgent Bill",
cycleStart: new Date().toISOString(),
dueOn: new Date(Date.now() + 10 * 86400000).toISOString(), // due in 10 days
totalCents: 80000n, // $800 - more than the 50% allocation ($500)
fundedCents: 0n,
currentFundedCents: 0n,
priority: 1,
fundingMode: "auto-on-deposit",
},
});
const result = await allocateBudget(prisma as any, U, 100000, 50); // 50% to fixed for crisis testing
// Crisis should be detected (10 days < 14 day window for irregular income)
expect(result.crisis.active).toBe(true);
expect(result.crisis.plans.length).toBeGreaterThan(0);
const urgentPlan = result.crisis.plans.find(p => p.id === p1);
expect(urgentPlan).toBeDefined();
});
it("creates and updates budget session", async () => {
const c1 = cid("test");
await prisma.variableCategory.create({
data: { id: c1, userId: U, name: "Test", percent: 100, priority: 1, isSavings: false, balanceCents: 0n },
});
const result = await allocateBudget(prisma as any, U, 200000, 40); // 40% to fixed expenses
expect(result.totalBudgetCents).toBe(200000);
expect(result.availableBudgetCents).toBeGreaterThan(0);
});
it("handles zero budget gracefully", async () => {
const c1 = cid("test");
await prisma.variableCategory.create({
data: { id: c1, userId: U, name: "Test", percent: 100, priority: 1, isSavings: false, balanceCents: 0n },
});
const result = await allocateBudget(prisma as any, U, 0, 30); // 30% to fixed expenses
expect(result.totalBudgetCents).toBe(0);
expect(result.fundedBudgetCents).toBe(0);
expect(result.availableBudgetCents).toBe(0);
expect(result.fixedAllocations).toHaveLength(0);
expect(result.variableAllocations).toHaveLength(0);
});
});
});