Files
OnBudget/test/features/notification_parsing/parser/decision_gate_test.dart
T
SandersandClaude Opus 4.8 4f99b80169 Replace account_bindings with per-app default account; build analytics charts
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>
2026-07-18 00:09:12 +03:00

212 lines
6.2 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:new_budget/src/core/database/converters/enum_converters.dart';
import 'package:new_budget/src/features/notification_parsing/data/parser/account_resolver.dart';
import 'package:new_budget/src/features/notification_parsing/data/parser/decision_gate.dart';
import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_draft.dart';
import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_rule.dart';
import 'package:new_budget/src/features/notification_parsing/domain/entities/raw_message.dart';
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
ParseRule _rule({TransactionType? txType = TransactionType.expense}) =>
ParseRule(
id: 'r1',
userId: 'u1',
kind: ParseRuleKind.merchantToCategory,
matchMode: MatchMode.contains,
pattern: 'PYATEROCHKA',
txType: txType,
categoryId: 'c1',
createdAt: DateTime(2026, 1, 1),
);
RawMessage _message({String body = 'PYATEROCHKA Покупка 1 240 ₽'}) =>
RawMessage(
id: 'm1',
userId: 'u1',
packageName: 'com.bank',
body: body,
receivedAt: DateTime(2026, 1, 2),
dedupHash: 'h1',
status: RawMessageStatus.pending,
createdAt: DateTime(2026, 1, 2),
);
ParseDraft _draft({
int amount = 124000,
TransactionType type = TransactionType.expense,
String currency = 'RUB',
String? accountId = 'a1',
}) =>
ParseDraft(
rawMessageId: 'm1',
type: type,
amount: amount,
currency: currency,
merchantRaw: 'PYATEROCHKA',
accountId: accountId,
source: ParseSource.ai,
);
const _trusted = AccountResolution(
accountId: 'a1',
score: 90,
trusted: true,
source: AccountSource.senderRule,
);
const _untrusted = AccountResolution(
accountId: 'a1',
score: 40,
trusted: false,
source: AccountSource.globalDefault,
);
void main() {
group('decide', () {
test('rule + amount in body + trusted account → autoApply', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.autoApply);
expect(r.failed, isEmpty);
});
test('toggle off → inbox without evaluating checks', () {
final r = decide(
autoApplyEnabled: false,
merchantRule: _rule(),
draft: _draft(),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, isEmpty);
});
test('no rule → inbox with ruleMatched failed', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: null,
draft: _draft(),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.ruleMatched});
});
test('amount not found in body → inbox', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(amount: 99900),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.amountVerifiedInBody});
});
test('empty currency → inbox', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(currency: ''),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.currencyKnown});
});
test('draft type differs from rule txType → inbox', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(txType: TransactionType.expense),
draft: _draft(type: TransactionType.income),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.typeMatchesRule});
});
test('legacy rule without txType → type check skipped, autoApply', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(txType: null),
draft: _draft(type: TransactionType.income),
message: _message(),
resolution: _trusted,
);
expect(r.decision, GateDecision.autoApply);
});
test('no account → inbox even with rule', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(accountId: null),
message: _message(),
resolution: const AccountResolution(
accountId: null,
score: 15,
trusted: false,
source: AccountSource.none,
),
);
expect(r.decision, GateDecision.inbox);
expect(
r.failed,
{AutoApplyCheck.accountResolved, AutoApplyCheck.accountTrusted},
);
});
test('untrusted account (global default) → inbox', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(),
message: _message(),
resolution: _untrusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.accountTrusted});
});
test('huge amount → inbox even with rule', () {
const amount = 100001 * 100;
final r = decide(
autoApplyEnabled: true,
merchantRule: _rule(),
draft: _draft(amount: amount),
message: _message(body: 'PYATEROCHKA Покупка 100 001 ₽'),
resolution: _trusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {AutoApplyCheck.amountUnderCap});
});
test('collects all failed checks, not just the first', () {
final r = decide(
autoApplyEnabled: true,
merchantRule: null,
draft: _draft(amount: 99900, currency: ''),
message: _message(),
resolution: _untrusted,
);
expect(r.decision, GateDecision.inbox);
expect(r.failed, {
AutoApplyCheck.ruleMatched,
AutoApplyCheck.amountVerifiedInBody,
AutoApplyCheck.currencyKnown,
AutoApplyCheck.accountTrusted,
});
});
});
}