import { describe, expect, it } from 'vitest'; import { calculateNetWorth, calculateBalanceByType, calculateNetWorthTrend } from '../src/domain/netWorth'; import { generateAnnualReport } from '../src/domain/annualReport'; import { buildAccountTree, calculateAccountTotalBalance, calculateReport } from '../src/domain/accountTree'; import { parseLedger } from '../src/domain/ledger'; import type { Transaction } from '../src/domain/types'; const tx = (over: Partial): Transaction => ({ id: 't1', date: '2026-07-10', flag: '*', postings: [], tags: [], links: [], source: 'mobile.bean', raw: '', ...over, }); const sampleTx: Transaction[] = [ tx({ id: 't1', date: '2026-01-15', postings: [{ account: 'Assets:Alipay', amount: '-100', currency: 'CNY' }, { account: 'Expenses:Food', amount: '100', currency: 'CNY' }] }), tx({ id: 't2', date: '2026-01-20', postings: [{ account: 'Assets:Bank', amount: '5000', currency: 'CNY' }, { account: 'Income:Salary', amount: '-5000', currency: 'CNY' }] }), tx({ id: 't3', date: '2026-02-10', postings: [{ account: 'Assets:Alipay', amount: '-200', currency: 'CNY' }, { account: 'Expenses:Food', amount: '200', currency: 'CNY' }] }), tx({ id: 't4', date: '2026-03-05', postings: [{ account: 'Liabilities:CreditCard', amount: '-50', currency: 'CNY' }, { account: 'Expenses:Food', amount: '50', currency: 'CNY' }] }), ]; describe('netWorth', () => { it('calculateBalanceByType', () => { expect(calculateBalanceByType(sampleTx, 'Assets')).toBe('4700'); // -100+5000-200 = 4700 expect(calculateBalanceByType(sampleTx, 'Liabilities')).toBe('-50'); }); it('calculateNetWorth', () => { const nw = calculateNetWorth(sampleTx); expect(nw.assets).toBe('4700'); expect(nw.liabilities).toBe('50'); expect(nw.netWorth).toBe('4650'); }); it('calculateNetWorthTrend', () => { const trend = calculateNetWorthTrend(sampleTx, ['2026-01-31', '2026-02-28']); expect(trend).toHaveLength(2); expect(trend[0].netWorth).toBe('4900'); // 截至1/31: -100+5000 = 4900 资产 expect(parseFloat(trend[1].netWorth)).toBe(4700); }); }); describe('annualReport', () => { it('generateAnnualReport 2026', () => { const report = generateAnnualReport(sampleTx, 2026); expect(report.year).toBe(2026); expect(report.transactionCount).toBe(4); expect(report.totalExpense).toBe('350'); // 100+200+50 expect(report.totalIncome).toBe('5000'); expect(report.topCategories.length).toBeGreaterThan(0); expect(report.topCategories[0].category).toContain('Food'); expect(report.monthlyTrend).toHaveLength(12); expect(report.dayOfWeekBreakdown).toHaveLength(7); }); it('无数据年份返回空', () => { const report = generateAnnualReport(sampleTx, 2025); expect(report.transactionCount).toBe(0); expect(report.totalExpense).toBe('0'); }); }); describe('accountTree', () => { it('buildAccountTree + calculateAccountTotalBalance', () => { const ledger = parseLedger([{ path: 'main.bean', content: '2026-01-01 open Assets:Alipay CNY\n2026-01-01 open Assets:Bank:CMB CNY\n2026-01-01 open Expenses:Food CNY\n' }]); const nodes = buildAccountTree(ledger.accounts, sampleTx); // 顶层应有 Assets、Expenses(Income/Liabilities 因无账户注册可能不出现) const topNames = nodes.map(n => n.name); expect(topNames).toContain('Assets'); expect(topNames).toContain('Expenses'); // Assets 总余额 const assets = nodes.find(n => n.name === 'Assets'); expect(calculateAccountTotalBalance(assets!)).toBe('4700'); }); it('calculateReport', () => { const ledger = parseLedger([{ path: 'main.bean', content: '' }]); const report = calculateReport({ ...ledger, files: [], transactions: sampleTx }); expect(report.expense).toBe('350'); expect(report.income).toBe('5000'); }); });