Add transfer tracking and secure admin access

This commit is contained in:
2026-07-26 11:57:57 +08:00
parent 0738953e6d
commit 7df25edd96
111 changed files with 6379 additions and 1934 deletions
+26
View File
@@ -65,6 +65,32 @@ void main() {
expect(ledger.transactionCount, 9);
});
test('transfer DTO uses direction for effective income and expense', () {
TxItem transfer(String direction) => TxItem.fromJson({
'id': direction == 'in' ? 8 : 9,
'ledgerId': 3,
'categoryId': direction == 'in' ? 101 : 1,
'categoryName': direction == 'in' ? '工资' : '餐饮',
'categoryIcon': direction == 'in' ? 'money' : 'food',
'type': 'transfer',
'transferDirection': direction,
'counterparty': '张三',
'amount': 20,
'source': 'accessibility',
'occurredAt': '2026-07-25T03:00:00Z',
});
final transferIn = transfer('in');
final transferOut = transfer('out');
expect(transferIn.isIncome, isTrue);
expect(transferIn.isExpense, isFalse);
expect(transferIn.typeLabel, '转入');
expect(transferOut.isExpense, isTrue);
expect(transferOut.isIncome, isFalse);
expect(transferOut.typeLabel, '转出');
expect(transferOut.counterparty, '张三');
});
test('budget refinement DTO keeps conversation summary and constraints', () {
final recommendation = BudgetRecommendations.fromJson({
'year': 2026,
@@ -0,0 +1,85 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:miaoji_zhang/shared/services/local_database.dart';
void main() {
test('本地转账按方向统计并按稳定身份幂等', () {
final database = LocalDatabase.inMemoryForTesting();
addTearDown(database.close);
database.upsertBudget(2026, 7, 1, null, 100, false);
database.upsertBudget(2026, 7, 1, 1, 80, false);
final transferOut = <String, dynamic>{
'ledgerId': 1,
'categoryId': 1,
'type': 'transfer',
'transferDirection': 'out',
'counterparty': '张三',
'amount': 40,
'occurredAt': '2026-07-25T03:00:00.000Z',
'source': 'accessibility',
'clientRequestId': 'wx-event-out-a',
'provider': 'wechat',
'providerTransactionId': 'wx-order-out-001',
'recognitionOccurrenceId': 'wx-occurrence-out-a',
};
final createdOut = database.createTransaction(transferOut);
final providerRetry = database.createTransaction({
...transferOut,
'clientRequestId': 'wx-event-out-b',
'recognitionOccurrenceId': 'wx-occurrence-out-b',
});
expect(providerRetry['id'], createdOut['id']);
final transferIn = <String, dynamic>{
'ledgerId': 1,
'categoryId': 101,
'type': 'transfer',
'transferDirection': 'in',
'counterparty': '张三',
'amount': 75,
'occurredAt': '2026-07-25T03:05:00.000Z',
'source': 'accessibility',
'clientRequestId': 'wx-event-in-a',
'recognitionOccurrenceId': 'wx-occurrence-in-001',
};
final createdIn = database.createTransaction(transferIn);
final occurrenceRetry = database.createTransaction({
...transferIn,
'clientRequestId': 'wx-event-in-b',
});
expect(occurrenceRetry['id'], createdIn['id']);
final month = database.monthSummary(2026, 7, 1);
expect(month['count'], 2);
expect(month['expense'], 40);
expect(month['income'], 75);
expect(month['balance'], 35);
final stats = database.periodStats('month', DateTime(2026, 7, 25), 1);
expect(stats['totalExpense'], 40);
expect(stats['totalIncome'], 75);
final budgets = database.budgets(2026, 7, 1);
expect((budgets['total'] as Map<String, dynamic>)['spent'], 40);
expect(
((budgets['categories'] as List).single as Map<String, dynamic>)['spent'],
40,
);
});
test('本地数据库拒绝缺少方向的转账', () {
final database = LocalDatabase.inMemoryForTesting();
addTearDown(database.close);
expect(
() => database.createTransaction({
'ledgerId': 1,
'categoryId': 1,
'type': 'transfer',
'amount': 20,
'occurredAt': '2026-07-25T03:00:00.000Z',
}),
throwsStateError,
);
});
}