DriftLedger/tests/domain-finance.test.ts
fengmengqi f6437b83fe feat: 初始化完整应用框架
- 切换到 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+ 单元测试覆盖核心逻辑
2026-07-15 10:01:31 +08:00

132 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest';
import { calculateBudgetProgress, getPeriodRange, type Budget } from '../src/domain/budgets';
import { calculateNextDueDate, getDueRecurring, instantiateRecurring, type RecurringTransaction } from '../src/domain/recurring';
import { buildTransactionCurrency, convertCurrency, getEffectiveRate, validateRateCoverage, type ExchangeRate } from '../src/domain/currency';
import { calculateBillingPeriod, calculateStatementAmount, calculateAvailableCredit, type CreditCard } from '../src/domain/creditCards';
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,
});
describe('budgets', () => {
const budget: Budget = { id: 'b1', name: '餐饮', amount: '1000', period: 'monthly', categoryAccount: 'Expenses:Food', startDate: '2026-01-01' };
const transactions = [
tx({ id: 't1', date: '2026-07-05', postings: [{ account: 'Assets:Alipay', amount: '-100', currency: 'CNY' }, { account: 'Expenses:Food', amount: '100', currency: 'CNY' }] }),
tx({ id: 't2', date: '2026-07-10', postings: [{ account: 'Assets:Alipay', amount: '-200', currency: 'CNY' }, { account: 'Expenses:Food', amount: '200', currency: 'CNY' }] }),
tx({ id: 't3', date: '2026-07-15', postings: [{ account: 'Assets:Alipay', amount: '-50', currency: 'CNY' }, { account: 'Expenses:Transport', amount: '50', currency: 'CNY' }] }),
];
it('计算预算进度', () => {
const progress = calculateBudgetProgress(budget, transactions, '2026-07-15');
expect(progress.spent).toBe('300'); // 100+200Food
expect(progress.percentage).toBe(30); // 300/1000
expect(progress.overBudget).toBe(false);
});
it('超支检测', () => {
const smallBudget = { ...budget, amount: '200' };
const progress = calculateBudgetProgress(smallBudget, transactions, '2026-07-15');
expect(progress.overBudget).toBe(true);
});
it('getPeriodRange monthly', () => {
const range = getPeriodRange('monthly', '2026-07-15');
expect(range.start).toBe('2026-07-01');
expect(range.end).toBe('2026-07-31');
});
it('getPeriodRange yearly', () => {
const range = getPeriodRange('yearly', '2026-07-15');
expect(range.start).toBe('2026-01-01');
expect(range.end).toBe('2026-12-31');
});
});
describe('recurring', () => {
const r: RecurringTransaction = {
id: 'r1', name: '房租', frequency: 'monthly', interval: 1, nextDueDate: '2026-07-01', enabled: true,
draft: { date: '2026-07-01', narration: '房租', postings: [{ account: 'Assets:Bank', amount: '-3000', currency: 'CNY' }, { account: 'Expenses:Rent', amount: '3000', currency: 'CNY' }] },
};
it('getDueRecurring 检测到期', () => {
expect(getDueRecurring([r], '2026-07-01')).toHaveLength(1);
expect(getDueRecurring([r], '2026-06-30')).toHaveLength(0);
});
it('calculateNextDueDate monthly', () => {
expect(calculateNextDueDate('monthly', 1, '2026-07-01')).toBe('2026-08-01');
});
it('calculateNextDueDate weekly', () => {
expect(calculateNextDueDate('weekly', 2, '2026-07-01')).toBe('2026-07-15');
});
it('instantiateRecurring', () => {
const draft = instantiateRecurring(r);
expect(draft.date).toBe('2026-07-01');
});
});
describe('currency', () => {
const rates: ExchangeRate[] = [{ from: 'USD', to: 'CNY', rate: '7.2', date: '2026-07-13' }];
it('convertCurrency', () => {
expect(convertCurrency('100', 'USD', 'CNY', rates)).toBe('720.00');
});
it('同币种不转换', () => {
expect(convertCurrency('100', 'CNY', 'CNY', rates)).toBe('100');
});
it('getEffectiveRate 手动覆盖优先', () => {
const overrides = [{ from: 'USD', to: 'CNY', rate: '7.0', enabled: true }];
const rate = getEffectiveRate('USD', 'CNY', rates, overrides);
expect(rate?.rate).toBe('7.0');
});
it('buildTransactionCurrency 同币种', () => {
const tc = buildTransactionCurrency('100', 'CNY', 'CNY', rates);
expect(tc.exchangeRate).toBe('1');
});
it('buildTransactionCurrency 跨币种', () => {
const tc = buildTransactionCurrency('100', 'USD', 'CNY', rates);
expect(tc.exchangeRate).toBe('7.2');
});
it('validateRateCoverage', () => {
const missing = validateRateCoverage('CNY', ['CNY', 'USD', 'EUR'], rates);
expect(missing).toEqual(['EUR']);
});
});
describe('creditCards', () => {
const card: CreditCard = { id: 'c1', name: '招行信用卡', lastFour: '1234', bankName: 'CMB', billingDay: 5, paymentDay: 25, creditLimit: '10000', currency: 'CNY', linkedAccount: 'Liabilities:CreditCard:CMB' };
const transactions = [
tx({ id: 't1', date: '2026-07-10', postings: [{ account: 'Liabilities:CreditCard:CMB', amount: '-100', currency: 'CNY' }, { account: 'Expenses:Food', amount: '100', currency: 'CNY' }] }),
tx({ id: 't2', date: '2026-07-15', postings: [{ account: 'Liabilities:CreditCard:CMB', amount: '-200', currency: 'CNY' }, { account: 'Expenses:Food', amount: '200', currency: 'CNY' }] }),
];
it('calculateBillingPeriod', () => {
const period = calculateBillingPeriod(card, new Date('2026-07-20'));
expect(period.periodStart).toBe('2026-06-05');
expect(period.periodEnd).toBe('2026-07-05');
expect(period.dueDate).toBe('2026-07-25');
});
it('calculateStatementAmount', () => {
// 7月10日、15日 不在 6/5~7/5 周期内 → 0
const period = calculateBillingPeriod(card, new Date('2026-07-20'));
expect(calculateStatementAmount(transactions, card, period)).toBe('0');
// 用 8/20 周期7/5~8/5→ 包含 7/10、7/15
const period2 = calculateBillingPeriod(card, new Date('2026-08-20'));
expect(calculateStatementAmount(transactions, card, period2)).toBe('300');
});
it('calculateAvailableCredit', () => {
expect(calculateAvailableCredit(card, '-500')).toBe('9500.00');
});
});