import { describe, it } from 'node:test' import assert from 'node:assert' import { getTotalBudget, getTotalSpent, getRemaining, getProgressPercent, addTransaction, removeTransaction, rollOverMonth, computeLifestyleCreep, getNetWorth, } from '../src/lib/budget.js' import type { Category, Transaction } from '../src/types.js' const baseCats: Category[] = [ { id: 'food', name: 'Food', budget: 500, spent: 100, color: '#4CAF50', isSinkingFund: false, rolloverEnabled: false, rolloverAmount: 0 }, { id: 'vacation', name: 'Vacation', budget: 200, spent: 0, color: '#00BCD4', isSinkingFund: true, rolloverEnabled: true, rolloverAmount: 50 }, ] describe('getTotalBudget', () => { it('sums category budgets including rollover', () => { assert.strictEqual(getTotalBudget(baseCats), 750) }) }) describe('getTotalSpent', () => { it('sums category spending', () => { assert.strictEqual(getTotalSpent(baseCats), 100) }) }) describe('getRemaining', () => { it('computes remaining for non-sinking fund', () => { assert.strictEqual(getRemaining(baseCats[0]), 400) }) it('includes rollover for sinking fund', () => { assert.strictEqual(getRemaining(baseCats[1]), 250) }) }) describe('getProgressPercent', () => { it('returns correct percent', () => { assert.strictEqual(getProgressPercent(baseCats[0]), 20) }) it('caps at 100', () => { const cat: Category = { ...baseCats[0], spent: 600 } assert.strictEqual(getProgressPercent(cat), 100) }) }) describe('addTransaction', () => { it('adds a simple transaction', () => { const tx: Transaction = { id: '1', date: '2024-01-01', amount: 50, merchant: 'Store', categoryId: 'food', description: '', isSplit: false } const result = addTransaction(baseCats, [], tx) assert.strictEqual(result.categories[0].spent, 150) assert.strictEqual(result.transactions.length, 1) }) it('adds a split transaction', () => { const tx: Transaction = { id: '2', date: '2024-01-01', amount: 100, merchant: 'Amazon', categoryId: 'food', description: '', isSplit: true, items: [{ name: 'Food', amount: 60, categoryId: 'food' }, { name: 'Thing', amount: 40, categoryId: 'vacation' }], originalAmount: 100, } const result = addTransaction(baseCats, [], tx) assert.strictEqual(result.categories[0].spent, 160) assert.strictEqual(result.categories[1].spent, 40) }) }) describe('removeTransaction', () => { it('removes a transaction and reverts spending', () => { const tx: Transaction = { id: '1', date: '2024-01-01', amount: 50, merchant: 'Store', categoryId: 'food', description: '', isSplit: false } const added = addTransaction(baseCats, [], tx) const result = removeTransaction(added.categories, added.transactions, '1') assert.strictEqual(result.categories[0].spent, 100) assert.strictEqual(result.transactions.length, 0) }) }) describe('rollOverMonth', () => { it('rolls over remaining to next month for sinking funds', () => { const cats = rollOverMonth(baseCats) assert.strictEqual(cats[1].rolloverAmount, 300) assert.strictEqual(cats[1].spent, 0) }) it('does not roll over non-sinking funds', () => { const cats = rollOverMonth(baseCats) assert.strictEqual(cats[0].rolloverAmount, 0) assert.strictEqual(cats[0].spent, 0) }) }) describe('computeLifestyleCreep', () => { it('computes year-over-year changes', () => { const snapshots = [ { month: '2023-06', categories: { food: { budget: 400, spent: 300 } }, netWorth: 10000 }, { month: '2024-06', categories: { food: { budget: 500, spent: 350 } }, netWorth: 12000 }, ] const creep = computeLifestyleCreep(snapshots, '2024-06') assert.strictEqual(creep.length, 1) assert.strictEqual(creep[0].changePercent, 25) assert.strictEqual(creep[0].isInflation, true) }) }) describe('getNetWorth', () => { it('sums all account balances', () => { const accounts = [{ id: '1', name: 'A', type: 'checking' as const, balance: 1000 }, { id: '2', name: 'B', type: 'savings' as const, balance: 2000 }] assert.strictEqual(getNetWorth(accounts), 3000) }) })