Files
SkyMoney/api/tests/irregular-income-simple.test.ts

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