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