180 lines
6.8 KiB
TypeScript
180 lines
6.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { allocateBudget, applyIrregularIncome, previewAllocation } from '../src/allocator.ts';
|
|
import { prisma, U, resetUser, ensureUser, cid, pid } from './helpers.ts';
|
|
|
|
describe('Irregular Income Allocation', () => {
|
|
const userId = U;
|
|
|
|
beforeEach(async () => {
|
|
await resetUser(userId);
|
|
await ensureUser(userId);
|
|
});
|
|
|
|
async function createFixedPlan(totalCents: number, name: string, priority: number, dueInDays: number) {
|
|
const id = pid();
|
|
const now = new Date();
|
|
await prisma.fixedPlan.create({
|
|
data: {
|
|
id,
|
|
userId,
|
|
name,
|
|
totalCents: BigInt(totalCents),
|
|
priority,
|
|
dueOn: new Date(Date.now() + dueInDays * 24 * 60 * 60 * 1000),
|
|
cycleStart: now, // Required field
|
|
},
|
|
});
|
|
return id;
|
|
}
|
|
|
|
async function createVariableCategory(name: string, percent: number, priority: number, isSavings = false) {
|
|
const id = cid();
|
|
await prisma.variableCategory.create({
|
|
data: {
|
|
id,
|
|
userId,
|
|
name,
|
|
percent,
|
|
priority,
|
|
isSavings,
|
|
},
|
|
});
|
|
return id;
|
|
}
|
|
|
|
async function createIncomeEvent(amountCents: number, daysAgo = 0) {
|
|
const id = `income_${Date.now()}_${Math.random()}`;
|
|
await prisma.incomeEvent.create({
|
|
data: {
|
|
id,
|
|
userId,
|
|
amountCents: BigInt(amountCents),
|
|
postedAt: new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000),
|
|
},
|
|
});
|
|
return id;
|
|
}
|
|
|
|
describe('Basic Allocation Logic', () => {
|
|
it('should allocate percentage of new income to fixed expenses', async () => {
|
|
// Setup: Create fixed plans and variable categories
|
|
const rentId = await createFixedPlan(50000, 'Rent', 1, 10); // $500, due in 10 days (reduced so both can get funding)
|
|
const carId = await createFixedPlan(40000, 'Car Payment', 2, 5); // $400, due in 5 days
|
|
|
|
await createVariableCategory('Groceries', 50, 1);
|
|
await createVariableCategory('Entertainment', 30, 2);
|
|
await createVariableCategory('Savings', 20, 3, true);
|
|
|
|
// Add some available budget
|
|
await createIncomeEvent(50000, 1); // $500 from 1 day ago
|
|
|
|
// Test: Allocate $2,000 with 30% to fixed expenses
|
|
const result = await allocateBudget(prisma, userId, 200000, 30);
|
|
|
|
expect(result.totalBudgetCents).toBe(200000); // $2,000 new income
|
|
|
|
// Fixed expenses should get 30% of new income = $600
|
|
const totalFixedAllocated = result.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
expect(totalFixedAllocated).toBe(60000); // $600
|
|
|
|
// Car payment (5 days, crisis for irregular) should get some allocation
|
|
const carAllocation = result.fixedAllocations.find(a => a.fixedPlanId === carId);
|
|
expect(carAllocation).toBeDefined();
|
|
expect(carAllocation!.amountCents).toBeGreaterThan(0);
|
|
|
|
// Variables should get available budget + remaining new income
|
|
const totalVariableAllocated = result.variableAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
expect(totalVariableAllocated).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
it('should treat plans due within 14 days as crisis', async () => {
|
|
// Setup: Create fixed plan due in 12 days
|
|
const urgentBillId = await createFixedPlan(80000, 'Urgent Bill', 1, 12); // $800, due in 12 days
|
|
|
|
const result = await allocateBudget(prisma, userId, 100000, 50); // $1,000 with 50% to fixed
|
|
|
|
expect(result.crisis.active).toBe(true);
|
|
expect(result.crisis.plans).toHaveLength(1);
|
|
expect(result.crisis.plans[0].id).toBe(urgentBillId);
|
|
expect(result.crisis.plans[0].daysUntilDue).toBe(12);
|
|
});
|
|
|
|
it('should work with different fixed expense percentages', async () => {
|
|
await createFixedPlan(100000, 'Rent', 1, 30); // $1,000
|
|
await createVariableCategory('Everything', 100, 1);
|
|
|
|
// Test 10% to fixed expenses
|
|
const result10 = await allocateBudget(prisma, userId, 200000, 10);
|
|
const fixed10 = result10.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
|
|
// Test 50% to fixed expenses
|
|
const result50 = await allocateBudget(prisma, userId, 200000, 50);
|
|
const fixed50 = result50.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
|
|
expect(fixed50).toBeGreaterThan(fixed10);
|
|
console.log('10% allocation to fixed:', fixed10);
|
|
console.log('50% allocation to fixed:', fixed50);
|
|
});
|
|
});
|
|
|
|
describe('Comparison with Regular Income', () => {
|
|
it('should show meaningful differences from regular income allocation', async () => {
|
|
// Setup same scenario for both
|
|
await createFixedPlan(120000, 'Rent', 1, 10); // $1,200, due in 10 days
|
|
await createFixedPlan(40000, 'Car Payment', 2, 5); // $400, due in 5 days
|
|
|
|
await createVariableCategory('Groceries', 60, 1);
|
|
await createVariableCategory('Savings', 40, 2, true);
|
|
|
|
// Add available budget
|
|
await createIncomeEvent(75000, 1); // $750 available
|
|
|
|
// Set user to biweekly frequency for regular income test
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: { incomeFrequency: 'biweekly' }
|
|
});
|
|
|
|
// Test regular income allocation
|
|
const regularResult = await previewAllocation(prisma, userId, 200000); // $2,000
|
|
|
|
// Test irregular income allocation
|
|
const irregularResult = await allocateBudget(prisma, userId, 200000, 30); // $2,000 with 30% fixed
|
|
|
|
// Compare results
|
|
const regularFixedTotal = regularResult.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
const irregularFixedTotal = irregularResult.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
|
|
// The strategies should be different
|
|
expect(regularFixedTotal).not.toBe(irregularFixedTotal);
|
|
|
|
console.log('Regular Income Fixed Allocation:', regularFixedTotal);
|
|
console.log('Irregular Income Fixed Allocation:', irregularFixedTotal);
|
|
console.log('Irregular should be capped at 30% = $600:', irregularFixedTotal <= 60000);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle 0% fixed expense allocation', async () => {
|
|
await createFixedPlan(100000, 'Rent', 1, 30);
|
|
await createVariableCategory('Everything', 100, 1);
|
|
|
|
const result = await allocateBudget(prisma, userId, 200000, 0); // 0% to fixed
|
|
|
|
const fixedTotal = result.fixedAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
expect(fixedTotal).toBe(0);
|
|
});
|
|
|
|
it('should handle no fixed expenses', async () => {
|
|
await createVariableCategory('Everything', 100, 1);
|
|
|
|
const result = await allocateBudget(prisma, userId, 200000, 30);
|
|
|
|
expect(result.fixedAllocations).toHaveLength(0);
|
|
const variableTotal = result.variableAllocations.reduce((sum, a) => sum + a.amountCents, 0);
|
|
expect(variableTotal).toBe(200000); // All income to variables
|
|
});
|
|
});
|
|
}); |