Notification parsing: - Drop the account_bindings table/DAO/repo/entity/controller/screen; account resolution now goes senderToAccount rule -> source_apps.defaultAccountId (trusted) -> global default (untrusted -> Inbox), via v1->v2 migration. - Add defaultAccountId to source_apps; per-app settings consolidated into source_app_detail_screen (/settings/parsing/apps/:pkg). - Inbox auto-learns an app default on first Confirm/CreateRule; account picker on the card instead of a disabled button; parse_error_labels extracted. Analytics: - Replace placeholder screen with fl_chart cards (chart_card, chart_theme, month_stepper, month_math domain helper); slim down habit_analysis_screen. Android: add launcher icon (adaptive foreground + colors.xml) and app_name. Tests: migration_v2, analytics (screen/month_math), AI retry, inbox visibility; update resolver/gate/inbox suites for the new resolution path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
2.9 KiB
Dart
88 lines
2.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/data/parser/account_resolver.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_rule.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
|
|
|
|
const _userId = 'u1';
|
|
final _now = DateTime(2026, 1, 1);
|
|
|
|
ParseRule _senderRule(String pattern, String accountId, {bool enabled = true}) =>
|
|
ParseRule(
|
|
id: 'sr-$pattern',
|
|
userId: _userId,
|
|
kind: ParseRuleKind.senderToAccount,
|
|
matchMode: MatchMode.contains,
|
|
pattern: pattern,
|
|
accountId: accountId,
|
|
enabled: enabled,
|
|
createdAt: _now,
|
|
);
|
|
|
|
AccountResolution _resolve({
|
|
List<ParseRule> senderRules = const [],
|
|
String? appDefaultAccountId,
|
|
String? globalDefaultAccountId,
|
|
String body = 'Покупка 1240 ₽ Карта *3456 PYATEROCHKA',
|
|
}) =>
|
|
resolveAccount(
|
|
body: body,
|
|
senderRules: senderRules,
|
|
appDefaultAccountId: appDefaultAccountId,
|
|
globalDefaultAccountId: globalDefaultAccountId,
|
|
);
|
|
|
|
void main() {
|
|
group('resolveAccount (лестница §B)', () {
|
|
test('#1 senderToAccount rule → 90, trusted; бьёт оба дефолта', () {
|
|
final r = _resolve(
|
|
senderRules: [_senderRule('*3456', 'acc-rule')],
|
|
appDefaultAccountId: 'acc-app',
|
|
globalDefaultAccountId: 'acc-global',
|
|
);
|
|
expect(r.accountId, 'acc-rule');
|
|
expect(r.score, 90);
|
|
expect(r.trusted, isTrue);
|
|
expect(r.source, AccountSource.senderRule);
|
|
});
|
|
|
|
test('#1 несовпавшее/выключенное правило пропускается', () {
|
|
final r = _resolve(
|
|
senderRules: [
|
|
_senderRule('НЕ СОВПАДЁТ', 'acc-miss'),
|
|
_senderRule('*3456', 'acc-off', enabled: false),
|
|
],
|
|
appDefaultAccountId: 'acc-app',
|
|
);
|
|
expect(r.accountId, 'acc-app');
|
|
expect(r.source, AccountSource.appDefault);
|
|
});
|
|
|
|
test('#2 дефолт приложения → 75, trusted', () {
|
|
final r = _resolve(
|
|
appDefaultAccountId: 'acc-app',
|
|
globalDefaultAccountId: 'acc-global',
|
|
);
|
|
expect(r.accountId, 'acc-app');
|
|
expect(r.score, 75);
|
|
expect(r.trusted, isTrue);
|
|
expect(r.source, AccountSource.appDefault);
|
|
});
|
|
|
|
test('#3 глобальный дефолт → 40, NOT trusted (смена поведения)', () {
|
|
final r = _resolve(globalDefaultAccountId: 'acc-global');
|
|
expect(r.accountId, 'acc-global');
|
|
expect(r.score, 40);
|
|
expect(r.trusted, isFalse);
|
|
expect(r.source, AccountSource.globalDefault);
|
|
});
|
|
|
|
test('#4 ничего → null, 15, NOT trusted', () {
|
|
final r = _resolve();
|
|
expect(r.accountId, isNull);
|
|
expect(r.score, 15);
|
|
expect(r.trusted, isFalse);
|
|
expect(r.source, AccountSource.none);
|
|
});
|
|
});
|
|
}
|