- 切换到 expo-router 文件路由,删除 App.tsx - 新增 5 个 Expo 原生插件:ppocr (OCR), accessibility (账单抓取), notification-listener, screenshot-monitor, sms-receiver - 实现核心领域逻辑:billPipeline (账单流水), dedup (去重), transferRecognizer (转账识别), ruleEngine + categories (双轨制分类), budgets, creditCards, recurring, sync, ocrProcessor - 增强 ledger.ts:支持 balance assertion, option, pad/note 指令, posting 级 metadata, cost/price 解析 - 新增完整 UI:tabs (首页/报表/设置), 交易详情, 预算, 日历热力图, 分类管理, 信用卡, 定期交易, 规则管理 - 实现 Zustand 状态管理:ledgerStore, importStore, settingsStore, metadataStore, automationStore + 持久化 - 新增 AI 功能:chatAssistant, monthlySummary, voiceInput - 实现多端同步:gitSync, webdavSync, icloudSync - 新增主题系统 (tokens/presets) 和 i18n (zh/en) - 添加 30+ 单元测试覆盖核心逻辑
81 lines
3.8 KiB
TypeScript
81 lines
3.8 KiB
TypeScript
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>): 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');
|
||
});
|
||
});
|