132 lines
4.8 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |