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