From 054bef832b41ccd6f8920ee2b450f747fd3760b7 Mon Sep 17 00:00:00 2001 From: Sanders Date: Sat, 30 May 2026 00:18:06 +0300 Subject: [PATCH] Stage 1 sms --- .gitignore | 2 +- design/flow.jpg | Bin 0 -> 283039 bytes lib/l10n/app_en.arb | 78 ++- lib/l10n/app_localizations.dart | 396 ++++++++++++++ lib/l10n/app_localizations_en.dart | 217 ++++++++ lib/l10n/app_localizations_ru.dart | 218 ++++++++ lib/l10n/app_ru.arb | 78 ++- lib/src/app/router/app_router.dart | 33 ++ lib/src/app/router/app_routes.dart | 9 + .../presentation/screens/home_screen.dart | 3 + .../presentation/widgets/month_header.dart | 35 +- .../application/inbox_controller.dart | 205 ++++++++ .../notification_parsing_providers.dart | 33 ++ .../parsing_settings_controller.dart | 58 ++ .../application/parsing_worker.dart | 221 ++++++++ .../application/rules_controller.dart | 90 ++++ .../bank_templates_catalog.dart | 63 +++ .../data/drift/daos/raw_messages_dao.dart | 11 + .../data/mappers/account_binding_mapper.dart | 17 + .../data/mappers/parse_rule_mapper.dart | 22 + .../data/mappers/raw_message_mapper.dart | 26 + .../data/mappers/rule_candidate_mapper.dart | 16 + .../data/parser/account_resolver.dart | 64 +++ .../data/parser/confidence_scorer.dart | 117 +++++ .../data/parser/decision_gate.dart | 35 ++ .../data/parser/dedup.dart | 28 + .../data/parser/draft_codec.dart | 94 ++++ .../data/parser/merchant_normalizer.dart | 17 + .../data/parser/regex_parser.dart | 95 ++++ .../data/parser/rule_lookup.dart | 81 +++ .../data/parser/rule_suggester.dart | 23 + .../account_bindings_repository_impl.dart | 87 +++ .../parse_rules_repository_impl.dart | 89 ++++ .../raw_messages_repository_impl.dart | 105 ++++ .../rule_candidates_repository_impl.dart | 60 +++ .../notification_parsing/domain/enums.dart | 1 + .../account_bindings_repository.dart | 40 ++ .../repositories/parse_rules_repository.dart | 39 ++ .../repositories/raw_messages_repository.dart | 54 ++ .../rule_candidates_repository.dart | 26 + .../screens/debug_inject_screen.dart | 140 +++++ .../presentation/screens/inbox_screen.dart | 114 ++++ .../screens/parsing_settings_screen.dart | 185 +++++++ .../screens/rule_editor_screen.dart | 497 ++++++++++++++++++ .../screens/rules_list_screen.dart | 139 +++++ .../widgets/confidence_badge.dart | 37 ++ .../presentation/widgets/inbox_card.dart | 389 ++++++++++++++ .../presentation/widgets/rule_card.dart | 103 ++++ .../presentation/screens/profile_screen.dart | 6 + .../application/inbox_controller_test.dart | 329 ++++++++++++ .../parser/confidence_scorer_test.dart | 130 +++++ .../parser/decision_gate_test.dart | 105 ++++ .../parser/draft_codec_test.dart | 67 +++ .../parser/misc_parser_test.dart | 49 ++ .../parser/regex_parser_test.dart | 100 ++++ .../parser/rule_lookup_test.dart | 124 +++++ .../presentation/inbox_card_test.dart | 177 +++++++ 57 files changed, 5568 insertions(+), 9 deletions(-) create mode 100644 design/flow.jpg create mode 100644 lib/src/features/notification_parsing/application/inbox_controller.dart create mode 100644 lib/src/features/notification_parsing/application/notification_parsing_providers.dart create mode 100644 lib/src/features/notification_parsing/application/parsing_settings_controller.dart create mode 100644 lib/src/features/notification_parsing/application/parsing_worker.dart create mode 100644 lib/src/features/notification_parsing/application/rules_controller.dart create mode 100644 lib/src/features/notification_parsing/data/bank_templates/bank_templates_catalog.dart create mode 100644 lib/src/features/notification_parsing/data/mappers/account_binding_mapper.dart create mode 100644 lib/src/features/notification_parsing/data/mappers/parse_rule_mapper.dart create mode 100644 lib/src/features/notification_parsing/data/mappers/raw_message_mapper.dart create mode 100644 lib/src/features/notification_parsing/data/mappers/rule_candidate_mapper.dart create mode 100644 lib/src/features/notification_parsing/data/parser/account_resolver.dart create mode 100644 lib/src/features/notification_parsing/data/parser/confidence_scorer.dart create mode 100644 lib/src/features/notification_parsing/data/parser/decision_gate.dart create mode 100644 lib/src/features/notification_parsing/data/parser/dedup.dart create mode 100644 lib/src/features/notification_parsing/data/parser/draft_codec.dart create mode 100644 lib/src/features/notification_parsing/data/parser/merchant_normalizer.dart create mode 100644 lib/src/features/notification_parsing/data/parser/regex_parser.dart create mode 100644 lib/src/features/notification_parsing/data/parser/rule_lookup.dart create mode 100644 lib/src/features/notification_parsing/data/parser/rule_suggester.dart create mode 100644 lib/src/features/notification_parsing/data/repositories/account_bindings_repository_impl.dart create mode 100644 lib/src/features/notification_parsing/data/repositories/parse_rules_repository_impl.dart create mode 100644 lib/src/features/notification_parsing/data/repositories/raw_messages_repository_impl.dart create mode 100644 lib/src/features/notification_parsing/data/repositories/rule_candidates_repository_impl.dart create mode 100644 lib/src/features/notification_parsing/domain/repositories/account_bindings_repository.dart create mode 100644 lib/src/features/notification_parsing/domain/repositories/parse_rules_repository.dart create mode 100644 lib/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart create mode 100644 lib/src/features/notification_parsing/domain/repositories/rule_candidates_repository.dart create mode 100644 lib/src/features/notification_parsing/presentation/screens/debug_inject_screen.dart create mode 100644 lib/src/features/notification_parsing/presentation/screens/inbox_screen.dart create mode 100644 lib/src/features/notification_parsing/presentation/screens/parsing_settings_screen.dart create mode 100644 lib/src/features/notification_parsing/presentation/screens/rule_editor_screen.dart create mode 100644 lib/src/features/notification_parsing/presentation/screens/rules_list_screen.dart create mode 100644 lib/src/features/notification_parsing/presentation/widgets/confidence_badge.dart create mode 100644 lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart create mode 100644 lib/src/features/notification_parsing/presentation/widgets/rule_card.dart create mode 100644 test/features/notification_parsing/application/inbox_controller_test.dart create mode 100644 test/features/notification_parsing/parser/confidence_scorer_test.dart create mode 100644 test/features/notification_parsing/parser/decision_gate_test.dart create mode 100644 test/features/notification_parsing/parser/draft_codec_test.dart create mode 100644 test/features/notification_parsing/parser/misc_parser_test.dart create mode 100644 test/features/notification_parsing/parser/regex_parser_test.dart create mode 100644 test/features/notification_parsing/parser/rule_lookup_test.dart create mode 100644 test/features/notification_parsing/presentation/inbox_card_test.dart diff --git a/.gitignore b/.gitignore index 155587a..037ef9c 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,4 @@ fastlane/screenshots/**/*.png fastlane/test_output # Design files (локальные макеты, не нужны в VCS) -design/ + diff --git a/design/flow.jpg b/design/flow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..697b1b47dafb6e74394e428c9c945327dc03e5b7 GIT binary patch literal 283039 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 K8cY1; literal 0 HcmV?d00001 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e6f5f43..c7c4c85 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -199,5 +199,81 @@ "accountValidationNameTooLong": "Name is too long (max 50 chars)", "accountSaveError": "Failed to save the account. Please try again.", "accountArchiveError": "Failed to archive the account. Please try again.", - "accountSetDefaultError": "Account saved, but couldn't mark it as default." + "accountSetDefaultError": "Account saved, but couldn't mark it as default.", + + "profileParsingTile": "Notification parsing", + + "parsingSettingsTitle": "Notification parsing", + "parsingSettingsSubtitle": "Settings", + "parsingEnableLabel": "Recognize notifications", + "parsingStrictnessLabel": "Auto-add strictness", + "parsingStrictnessSoft": "Soft", + "parsingStrictnessNormal": "Normal", + "parsingStrictnessStrict": "Strict", + "parsingStrictnessHint": "The threshold applies to amount, account and type — not to the merchant.", + "parsingRulesTile": "Parsing rules", + "parsingRulesCreatedCount": "Rules created: {count}", + "@parsingRulesCreatedCount": { "placeholders": { "count": { "type": "int" } } }, + "parsingAiSectionTitle": "AI (OpenRouter)", + "parsingAiComingSoon": "AI recognition is coming in a future version.", + "parsingDebugSectionTitle": "Debug", + "parsingDebugInjectTile": "Inject a test notification", + + "inboxTitle": "From notifications", + "inboxSubtitle": "A rule is learned on the first tap: similar messages will be confirmed automatically.", + "inboxEmpty": "Inbox is empty. New notifications will appear here.", + "inboxCreateRule": "Create rule “{merchant} → {category}”", + "@inboxCreateRule": { "placeholders": { "merchant": { "type": "String" }, "category": { "type": "String" } } }, + "inboxCreateRuleNoCategory": "Create rule for “{merchant}”", + "@inboxCreateRuleNoCategory": { "placeholders": { "merchant": { "type": "String" } } }, + "inboxConfirmOnce": "Confirm once", + "inboxIgnore": "Ignore", + "inboxApplyAll": "Apply all", + "inboxHideAll": "Hide all", + "inboxUnrecognized": "Not recognized", + "inboxAddManually": "Add manually", + "inboxNoCategory": "No category", + "inboxAccountUnknown": "Account unknown", + "inboxEditRuleTooltip": "Edit before saving", + + "rulesTitle": "Parsing rules", + "rulesSubtitle": "Settings", + "rulesFilterAll": "All", + "rulesFilterMerchants": "Merchants", + "rulesFilterAccounts": "Accounts", + "rulesFilterIgnore": "Ignore", + "rulesEmpty": "No rules yet. Create them from the Inbox in one tap.", + "ruleMatchesCount": "{count, plural, one{{count} match} other{{count} matches}}", + "@ruleMatchesCount": { "placeholders": { "count": { "type": "int" } } }, + "ruleDisabled": "Disabled", + + "ruleEditorNewTitle": "New rule", + "ruleEditorEditTitle": "Rule", + "ruleEditorIfContains": "If the notification contains", + "ruleEditorPatternHint": "e.g. WBSPB", + "ruleEditorModeContains": "Contains", + "ruleEditorModeExact": "Exact", + "ruleEditorModeRegex": "Regex", + "ruleEditorThen": "Then it is:", + "ruleEditorMerchant": "Merchant", + "ruleEditorMerchantHint": "e.g. Wildberries", + "ruleEditorCategory": "Category", + "ruleEditorAccount": "Account", + "ruleEditorAccountUnchanged": "— keep unchanged —", + "ruleEditorAdvanced": "Advanced", + "ruleEditorPriority": "Priority", + "ruleEditorMatchesTitle": "Matches (last 30 days)", + "ruleEditorNoMatches": "No matches in the last 30 days", + "ruleEditorSave": "Save", + "ruleEditorDelete": "Delete", + "ruleEditorEnabledLabel": "Enabled", + + "debugInjectTitle": "Test notification", + "debugInjectSubtitle": "Debug", + "debugInjectPackageLabel": "App package", + "debugInjectTitleLabel": "Title (optional)", + "debugInjectBodyLabel": "Notification text", + "debugInjectSend": "Send to pipeline", + "debugInjectSent": "Sent", + "debugInjectFillSample": "Sample: purchase" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f7ed29c..1b734e2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -883,6 +883,402 @@ abstract class AppLocalizations { /// In ru, this message translates to: /// **'Счёт сохранён, но не удалось сделать его умолчательным.'** String get accountSetDefaultError; + + /// No description provided for @profileParsingTile. + /// + /// In ru, this message translates to: + /// **'Парсинг уведомлений'** + String get profileParsingTile; + + /// No description provided for @parsingSettingsTitle. + /// + /// In ru, this message translates to: + /// **'Парсинг уведомлений'** + String get parsingSettingsTitle; + + /// No description provided for @parsingSettingsSubtitle. + /// + /// In ru, this message translates to: + /// **'Настройки'** + String get parsingSettingsSubtitle; + + /// No description provided for @parsingEnableLabel. + /// + /// In ru, this message translates to: + /// **'Распознавать уведомления'** + String get parsingEnableLabel; + + /// No description provided for @parsingStrictnessLabel. + /// + /// In ru, this message translates to: + /// **'Скорость авто-добавления'** + String get parsingStrictnessLabel; + + /// No description provided for @parsingStrictnessSoft. + /// + /// In ru, this message translates to: + /// **'Мягко'** + String get parsingStrictnessSoft; + + /// No description provided for @parsingStrictnessNormal. + /// + /// In ru, this message translates to: + /// **'Нормально'** + String get parsingStrictnessNormal; + + /// No description provided for @parsingStrictnessStrict. + /// + /// In ru, this message translates to: + /// **'Строго'** + String get parsingStrictnessStrict; + + /// No description provided for @parsingStrictnessHint. + /// + /// In ru, this message translates to: + /// **'Порог применяется к сумме, счёту и типу — не к мерчанту.'** + String get parsingStrictnessHint; + + /// No description provided for @parsingRulesTile. + /// + /// In ru, this message translates to: + /// **'Правила парсинга'** + String get parsingRulesTile; + + /// No description provided for @parsingRulesCreatedCount. + /// + /// In ru, this message translates to: + /// **'Правил создано: {count}'** + String parsingRulesCreatedCount(int count); + + /// No description provided for @parsingAiSectionTitle. + /// + /// In ru, this message translates to: + /// **'AI (OpenRouter)'** + String get parsingAiSectionTitle; + + /// No description provided for @parsingAiComingSoon. + /// + /// In ru, this message translates to: + /// **'ИИ-распознавание появится в следующей версии.'** + String get parsingAiComingSoon; + + /// No description provided for @parsingDebugSectionTitle. + /// + /// In ru, this message translates to: + /// **'Отладка'** + String get parsingDebugSectionTitle; + + /// No description provided for @parsingDebugInjectTile. + /// + /// In ru, this message translates to: + /// **'Вставить тестовое уведомление'** + String get parsingDebugInjectTile; + + /// No description provided for @inboxTitle. + /// + /// In ru, this message translates to: + /// **'Из уведомлений'** + String get inboxTitle; + + /// No description provided for @inboxSubtitle. + /// + /// In ru, this message translates to: + /// **'Правило обучится с первого раза: следующие похожие сообщения подтвердятся автоматически.'** + String get inboxSubtitle; + + /// No description provided for @inboxEmpty. + /// + /// In ru, this message translates to: + /// **'Inbox пуст. Новые уведомления появятся здесь.'** + String get inboxEmpty; + + /// No description provided for @inboxCreateRule. + /// + /// In ru, this message translates to: + /// **'Создать правило «{merchant} → {category}»'** + String inboxCreateRule(String merchant, String category); + + /// No description provided for @inboxCreateRuleNoCategory. + /// + /// In ru, this message translates to: + /// **'Создать правило для «{merchant}»'** + String inboxCreateRuleNoCategory(String merchant); + + /// No description provided for @inboxConfirmOnce. + /// + /// In ru, this message translates to: + /// **'Подтвердить разово'** + String get inboxConfirmOnce; + + /// No description provided for @inboxIgnore. + /// + /// In ru, this message translates to: + /// **'Игнорировать'** + String get inboxIgnore; + + /// No description provided for @inboxApplyAll. + /// + /// In ru, this message translates to: + /// **'Учесть все'** + String get inboxApplyAll; + + /// No description provided for @inboxHideAll. + /// + /// In ru, this message translates to: + /// **'Скрыть разово'** + String get inboxHideAll; + + /// No description provided for @inboxUnrecognized. + /// + /// In ru, this message translates to: + /// **'Не распознано'** + String get inboxUnrecognized; + + /// No description provided for @inboxAddManually. + /// + /// In ru, this message translates to: + /// **'Добавить вручную'** + String get inboxAddManually; + + /// No description provided for @inboxNoCategory. + /// + /// In ru, this message translates to: + /// **'Без категории'** + String get inboxNoCategory; + + /// No description provided for @inboxAccountUnknown. + /// + /// In ru, this message translates to: + /// **'Счёт не определён'** + String get inboxAccountUnknown; + + /// No description provided for @inboxEditRuleTooltip. + /// + /// In ru, this message translates to: + /// **'Изменить перед сохранением'** + String get inboxEditRuleTooltip; + + /// No description provided for @rulesTitle. + /// + /// In ru, this message translates to: + /// **'Правила парсинга'** + String get rulesTitle; + + /// No description provided for @rulesSubtitle. + /// + /// In ru, this message translates to: + /// **'Настройки'** + String get rulesSubtitle; + + /// No description provided for @rulesFilterAll. + /// + /// In ru, this message translates to: + /// **'Все'** + String get rulesFilterAll; + + /// No description provided for @rulesFilterMerchants. + /// + /// In ru, this message translates to: + /// **'Мерчанты'** + String get rulesFilterMerchants; + + /// No description provided for @rulesFilterAccounts. + /// + /// In ru, this message translates to: + /// **'Счета'** + String get rulesFilterAccounts; + + /// No description provided for @rulesFilterIgnore. + /// + /// In ru, this message translates to: + /// **'Игнор'** + String get rulesFilterIgnore; + + /// No description provided for @rulesEmpty. + /// + /// In ru, this message translates to: + /// **'Правил пока нет. Создавайте их из Inbox в один тап.'** + String get rulesEmpty; + + /// No description provided for @ruleMatchesCount. + /// + /// In ru, this message translates to: + /// **'{count, plural, one{{count} совпадение} few{{count} совпадения} many{{count} совпадений} other{{count} совпадений}}'** + String ruleMatchesCount(int count); + + /// No description provided for @ruleDisabled. + /// + /// In ru, this message translates to: + /// **'Выключено'** + String get ruleDisabled; + + /// No description provided for @ruleEditorNewTitle. + /// + /// In ru, this message translates to: + /// **'Новое правило'** + String get ruleEditorNewTitle; + + /// No description provided for @ruleEditorEditTitle. + /// + /// In ru, this message translates to: + /// **'Правило'** + String get ruleEditorEditTitle; + + /// No description provided for @ruleEditorIfContains. + /// + /// In ru, this message translates to: + /// **'Если уведомление содержит'** + String get ruleEditorIfContains; + + /// No description provided for @ruleEditorPatternHint. + /// + /// In ru, this message translates to: + /// **'напр. WBSPB'** + String get ruleEditorPatternHint; + + /// No description provided for @ruleEditorModeContains. + /// + /// In ru, this message translates to: + /// **'Содержит'** + String get ruleEditorModeContains; + + /// No description provided for @ruleEditorModeExact. + /// + /// In ru, this message translates to: + /// **'Точно'** + String get ruleEditorModeExact; + + /// No description provided for @ruleEditorModeRegex. + /// + /// In ru, this message translates to: + /// **'Regex'** + String get ruleEditorModeRegex; + + /// No description provided for @ruleEditorThen. + /// + /// In ru, this message translates to: + /// **'То это:'** + String get ruleEditorThen; + + /// No description provided for @ruleEditorMerchant. + /// + /// In ru, this message translates to: + /// **'Мерчант'** + String get ruleEditorMerchant; + + /// No description provided for @ruleEditorMerchantHint. + /// + /// In ru, this message translates to: + /// **'Напр. Wildberries'** + String get ruleEditorMerchantHint; + + /// No description provided for @ruleEditorCategory. + /// + /// In ru, this message translates to: + /// **'Категория'** + String get ruleEditorCategory; + + /// No description provided for @ruleEditorAccount. + /// + /// In ru, this message translates to: + /// **'Счёт'** + String get ruleEditorAccount; + + /// No description provided for @ruleEditorAccountUnchanged. + /// + /// In ru, this message translates to: + /// **'— не менять —'** + String get ruleEditorAccountUnchanged; + + /// No description provided for @ruleEditorAdvanced. + /// + /// In ru, this message translates to: + /// **'Дополнительно'** + String get ruleEditorAdvanced; + + /// No description provided for @ruleEditorPriority. + /// + /// In ru, this message translates to: + /// **'Приоритет'** + String get ruleEditorPriority; + + /// No description provided for @ruleEditorMatchesTitle. + /// + /// In ru, this message translates to: + /// **'Совпадает с (последние 30 дней)'** + String get ruleEditorMatchesTitle; + + /// No description provided for @ruleEditorNoMatches. + /// + /// In ru, this message translates to: + /// **'Нет совпадений за 30 дней'** + String get ruleEditorNoMatches; + + /// No description provided for @ruleEditorSave. + /// + /// In ru, this message translates to: + /// **'Сохранить'** + String get ruleEditorSave; + + /// No description provided for @ruleEditorDelete. + /// + /// In ru, this message translates to: + /// **'Удалить'** + String get ruleEditorDelete; + + /// No description provided for @ruleEditorEnabledLabel. + /// + /// In ru, this message translates to: + /// **'Активно'** + String get ruleEditorEnabledLabel; + + /// No description provided for @debugInjectTitle. + /// + /// In ru, this message translates to: + /// **'Тестовое уведомление'** + String get debugInjectTitle; + + /// No description provided for @debugInjectSubtitle. + /// + /// In ru, this message translates to: + /// **'Отладка'** + String get debugInjectSubtitle; + + /// No description provided for @debugInjectPackageLabel. + /// + /// In ru, this message translates to: + /// **'Пакет приложения'** + String get debugInjectPackageLabel; + + /// No description provided for @debugInjectTitleLabel. + /// + /// In ru, this message translates to: + /// **'Заголовок (необязательно)'** + String get debugInjectTitleLabel; + + /// No description provided for @debugInjectBodyLabel. + /// + /// In ru, this message translates to: + /// **'Текст уведомления'** + String get debugInjectBodyLabel; + + /// No description provided for @debugInjectSend. + /// + /// In ru, this message translates to: + /// **'Отправить в обработку'** + String get debugInjectSend; + + /// No description provided for @debugInjectSent. + /// + /// In ru, this message translates to: + /// **'Отправлено'** + String get debugInjectSent; + + /// No description provided for @debugInjectFillSample. + /// + /// In ru, this message translates to: + /// **'Пример: покупка'** + String get debugInjectFillSample; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1a415de..b7ad43f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -455,4 +455,221 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accountSetDefaultError => 'Account saved, but couldn\'t mark it as default.'; + + @override + String get profileParsingTile => 'Notification parsing'; + + @override + String get parsingSettingsTitle => 'Notification parsing'; + + @override + String get parsingSettingsSubtitle => 'Settings'; + + @override + String get parsingEnableLabel => 'Recognize notifications'; + + @override + String get parsingStrictnessLabel => 'Auto-add strictness'; + + @override + String get parsingStrictnessSoft => 'Soft'; + + @override + String get parsingStrictnessNormal => 'Normal'; + + @override + String get parsingStrictnessStrict => 'Strict'; + + @override + String get parsingStrictnessHint => + 'The threshold applies to amount, account and type — not to the merchant.'; + + @override + String get parsingRulesTile => 'Parsing rules'; + + @override + String parsingRulesCreatedCount(int count) { + return 'Rules created: $count'; + } + + @override + String get parsingAiSectionTitle => 'AI (OpenRouter)'; + + @override + String get parsingAiComingSoon => + 'AI recognition is coming in a future version.'; + + @override + String get parsingDebugSectionTitle => 'Debug'; + + @override + String get parsingDebugInjectTile => 'Inject a test notification'; + + @override + String get inboxTitle => 'From notifications'; + + @override + String get inboxSubtitle => + 'A rule is learned on the first tap: similar messages will be confirmed automatically.'; + + @override + String get inboxEmpty => + 'Inbox is empty. New notifications will appear here.'; + + @override + String inboxCreateRule(String merchant, String category) { + return 'Create rule “$merchant → $category”'; + } + + @override + String inboxCreateRuleNoCategory(String merchant) { + return 'Create rule for “$merchant”'; + } + + @override + String get inboxConfirmOnce => 'Confirm once'; + + @override + String get inboxIgnore => 'Ignore'; + + @override + String get inboxApplyAll => 'Apply all'; + + @override + String get inboxHideAll => 'Hide all'; + + @override + String get inboxUnrecognized => 'Not recognized'; + + @override + String get inboxAddManually => 'Add manually'; + + @override + String get inboxNoCategory => 'No category'; + + @override + String get inboxAccountUnknown => 'Account unknown'; + + @override + String get inboxEditRuleTooltip => 'Edit before saving'; + + @override + String get rulesTitle => 'Parsing rules'; + + @override + String get rulesSubtitle => 'Settings'; + + @override + String get rulesFilterAll => 'All'; + + @override + String get rulesFilterMerchants => 'Merchants'; + + @override + String get rulesFilterAccounts => 'Accounts'; + + @override + String get rulesFilterIgnore => 'Ignore'; + + @override + String get rulesEmpty => + 'No rules yet. Create them from the Inbox in one tap.'; + + @override + String ruleMatchesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count matches', + one: '$count match', + ); + return '$_temp0'; + } + + @override + String get ruleDisabled => 'Disabled'; + + @override + String get ruleEditorNewTitle => 'New rule'; + + @override + String get ruleEditorEditTitle => 'Rule'; + + @override + String get ruleEditorIfContains => 'If the notification contains'; + + @override + String get ruleEditorPatternHint => 'e.g. WBSPB'; + + @override + String get ruleEditorModeContains => 'Contains'; + + @override + String get ruleEditorModeExact => 'Exact'; + + @override + String get ruleEditorModeRegex => 'Regex'; + + @override + String get ruleEditorThen => 'Then it is:'; + + @override + String get ruleEditorMerchant => 'Merchant'; + + @override + String get ruleEditorMerchantHint => 'e.g. Wildberries'; + + @override + String get ruleEditorCategory => 'Category'; + + @override + String get ruleEditorAccount => 'Account'; + + @override + String get ruleEditorAccountUnchanged => '— keep unchanged —'; + + @override + String get ruleEditorAdvanced => 'Advanced'; + + @override + String get ruleEditorPriority => 'Priority'; + + @override + String get ruleEditorMatchesTitle => 'Matches (last 30 days)'; + + @override + String get ruleEditorNoMatches => 'No matches in the last 30 days'; + + @override + String get ruleEditorSave => 'Save'; + + @override + String get ruleEditorDelete => 'Delete'; + + @override + String get ruleEditorEnabledLabel => 'Enabled'; + + @override + String get debugInjectTitle => 'Test notification'; + + @override + String get debugInjectSubtitle => 'Debug'; + + @override + String get debugInjectPackageLabel => 'App package'; + + @override + String get debugInjectTitleLabel => 'Title (optional)'; + + @override + String get debugInjectBodyLabel => 'Notification text'; + + @override + String get debugInjectSend => 'Send to pipeline'; + + @override + String get debugInjectSent => 'Sent'; + + @override + String get debugInjectFillSample => 'Sample: purchase'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index d6c080f..f2c4dd7 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -467,4 +467,222 @@ class AppLocalizationsRu extends AppLocalizations { @override String get accountSetDefaultError => 'Счёт сохранён, но не удалось сделать его умолчательным.'; + + @override + String get profileParsingTile => 'Парсинг уведомлений'; + + @override + String get parsingSettingsTitle => 'Парсинг уведомлений'; + + @override + String get parsingSettingsSubtitle => 'Настройки'; + + @override + String get parsingEnableLabel => 'Распознавать уведомления'; + + @override + String get parsingStrictnessLabel => 'Скорость авто-добавления'; + + @override + String get parsingStrictnessSoft => 'Мягко'; + + @override + String get parsingStrictnessNormal => 'Нормально'; + + @override + String get parsingStrictnessStrict => 'Строго'; + + @override + String get parsingStrictnessHint => + 'Порог применяется к сумме, счёту и типу — не к мерчанту.'; + + @override + String get parsingRulesTile => 'Правила парсинга'; + + @override + String parsingRulesCreatedCount(int count) { + return 'Правил создано: $count'; + } + + @override + String get parsingAiSectionTitle => 'AI (OpenRouter)'; + + @override + String get parsingAiComingSoon => + 'ИИ-распознавание появится в следующей версии.'; + + @override + String get parsingDebugSectionTitle => 'Отладка'; + + @override + String get parsingDebugInjectTile => 'Вставить тестовое уведомление'; + + @override + String get inboxTitle => 'Из уведомлений'; + + @override + String get inboxSubtitle => + 'Правило обучится с первого раза: следующие похожие сообщения подтвердятся автоматически.'; + + @override + String get inboxEmpty => 'Inbox пуст. Новые уведомления появятся здесь.'; + + @override + String inboxCreateRule(String merchant, String category) { + return 'Создать правило «$merchant → $category»'; + } + + @override + String inboxCreateRuleNoCategory(String merchant) { + return 'Создать правило для «$merchant»'; + } + + @override + String get inboxConfirmOnce => 'Подтвердить разово'; + + @override + String get inboxIgnore => 'Игнорировать'; + + @override + String get inboxApplyAll => 'Учесть все'; + + @override + String get inboxHideAll => 'Скрыть разово'; + + @override + String get inboxUnrecognized => 'Не распознано'; + + @override + String get inboxAddManually => 'Добавить вручную'; + + @override + String get inboxNoCategory => 'Без категории'; + + @override + String get inboxAccountUnknown => 'Счёт не определён'; + + @override + String get inboxEditRuleTooltip => 'Изменить перед сохранением'; + + @override + String get rulesTitle => 'Правила парсинга'; + + @override + String get rulesSubtitle => 'Настройки'; + + @override + String get rulesFilterAll => 'Все'; + + @override + String get rulesFilterMerchants => 'Мерчанты'; + + @override + String get rulesFilterAccounts => 'Счета'; + + @override + String get rulesFilterIgnore => 'Игнор'; + + @override + String get rulesEmpty => + 'Правил пока нет. Создавайте их из Inbox в один тап.'; + + @override + String ruleMatchesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count совпадений', + many: '$count совпадений', + few: '$count совпадения', + one: '$count совпадение', + ); + return '$_temp0'; + } + + @override + String get ruleDisabled => 'Выключено'; + + @override + String get ruleEditorNewTitle => 'Новое правило'; + + @override + String get ruleEditorEditTitle => 'Правило'; + + @override + String get ruleEditorIfContains => 'Если уведомление содержит'; + + @override + String get ruleEditorPatternHint => 'напр. WBSPB'; + + @override + String get ruleEditorModeContains => 'Содержит'; + + @override + String get ruleEditorModeExact => 'Точно'; + + @override + String get ruleEditorModeRegex => 'Regex'; + + @override + String get ruleEditorThen => 'То это:'; + + @override + String get ruleEditorMerchant => 'Мерчант'; + + @override + String get ruleEditorMerchantHint => 'Напр. Wildberries'; + + @override + String get ruleEditorCategory => 'Категория'; + + @override + String get ruleEditorAccount => 'Счёт'; + + @override + String get ruleEditorAccountUnchanged => '— не менять —'; + + @override + String get ruleEditorAdvanced => 'Дополнительно'; + + @override + String get ruleEditorPriority => 'Приоритет'; + + @override + String get ruleEditorMatchesTitle => 'Совпадает с (последние 30 дней)'; + + @override + String get ruleEditorNoMatches => 'Нет совпадений за 30 дней'; + + @override + String get ruleEditorSave => 'Сохранить'; + + @override + String get ruleEditorDelete => 'Удалить'; + + @override + String get ruleEditorEnabledLabel => 'Активно'; + + @override + String get debugInjectTitle => 'Тестовое уведомление'; + + @override + String get debugInjectSubtitle => 'Отладка'; + + @override + String get debugInjectPackageLabel => 'Пакет приложения'; + + @override + String get debugInjectTitleLabel => 'Заголовок (необязательно)'; + + @override + String get debugInjectBodyLabel => 'Текст уведомления'; + + @override + String get debugInjectSend => 'Отправить в обработку'; + + @override + String get debugInjectSent => 'Отправлено'; + + @override + String get debugInjectFillSample => 'Пример: покупка'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 1377088..3f3278f 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -199,5 +199,81 @@ "accountValidationNameTooLong": "Слишком длинное название (макс. 50 символов)", "accountSaveError": "Не удалось сохранить счёт. Попробуйте ещё раз.", "accountArchiveError": "Не удалось архивировать счёт. Попробуйте ещё раз.", - "accountSetDefaultError": "Счёт сохранён, но не удалось сделать его умолчательным." + "accountSetDefaultError": "Счёт сохранён, но не удалось сделать его умолчательным.", + + "profileParsingTile": "Парсинг уведомлений", + + "parsingSettingsTitle": "Парсинг уведомлений", + "parsingSettingsSubtitle": "Настройки", + "parsingEnableLabel": "Распознавать уведомления", + "parsingStrictnessLabel": "Скорость авто-добавления", + "parsingStrictnessSoft": "Мягко", + "parsingStrictnessNormal": "Нормально", + "parsingStrictnessStrict": "Строго", + "parsingStrictnessHint": "Порог применяется к сумме, счёту и типу — не к мерчанту.", + "parsingRulesTile": "Правила парсинга", + "parsingRulesCreatedCount": "Правил создано: {count}", + "@parsingRulesCreatedCount": { "placeholders": { "count": { "type": "int" } } }, + "parsingAiSectionTitle": "AI (OpenRouter)", + "parsingAiComingSoon": "ИИ-распознавание появится в следующей версии.", + "parsingDebugSectionTitle": "Отладка", + "parsingDebugInjectTile": "Вставить тестовое уведомление", + + "inboxTitle": "Из уведомлений", + "inboxSubtitle": "Правило обучится с первого раза: следующие похожие сообщения подтвердятся автоматически.", + "inboxEmpty": "Inbox пуст. Новые уведомления появятся здесь.", + "inboxCreateRule": "Создать правило «{merchant} → {category}»", + "@inboxCreateRule": { "placeholders": { "merchant": { "type": "String" }, "category": { "type": "String" } } }, + "inboxCreateRuleNoCategory": "Создать правило для «{merchant}»", + "@inboxCreateRuleNoCategory": { "placeholders": { "merchant": { "type": "String" } } }, + "inboxConfirmOnce": "Подтвердить разово", + "inboxIgnore": "Игнорировать", + "inboxApplyAll": "Учесть все", + "inboxHideAll": "Скрыть разово", + "inboxUnrecognized": "Не распознано", + "inboxAddManually": "Добавить вручную", + "inboxNoCategory": "Без категории", + "inboxAccountUnknown": "Счёт не определён", + "inboxEditRuleTooltip": "Изменить перед сохранением", + + "rulesTitle": "Правила парсинга", + "rulesSubtitle": "Настройки", + "rulesFilterAll": "Все", + "rulesFilterMerchants": "Мерчанты", + "rulesFilterAccounts": "Счета", + "rulesFilterIgnore": "Игнор", + "rulesEmpty": "Правил пока нет. Создавайте их из Inbox в один тап.", + "ruleMatchesCount": "{count, plural, one{{count} совпадение} few{{count} совпадения} many{{count} совпадений} other{{count} совпадений}}", + "@ruleMatchesCount": { "placeholders": { "count": { "type": "int" } } }, + "ruleDisabled": "Выключено", + + "ruleEditorNewTitle": "Новое правило", + "ruleEditorEditTitle": "Правило", + "ruleEditorIfContains": "Если уведомление содержит", + "ruleEditorPatternHint": "напр. WBSPB", + "ruleEditorModeContains": "Содержит", + "ruleEditorModeExact": "Точно", + "ruleEditorModeRegex": "Regex", + "ruleEditorThen": "То это:", + "ruleEditorMerchant": "Мерчант", + "ruleEditorMerchantHint": "Напр. Wildberries", + "ruleEditorCategory": "Категория", + "ruleEditorAccount": "Счёт", + "ruleEditorAccountUnchanged": "— не менять —", + "ruleEditorAdvanced": "Дополнительно", + "ruleEditorPriority": "Приоритет", + "ruleEditorMatchesTitle": "Совпадает с (последние 30 дней)", + "ruleEditorNoMatches": "Нет совпадений за 30 дней", + "ruleEditorSave": "Сохранить", + "ruleEditorDelete": "Удалить", + "ruleEditorEnabledLabel": "Активно", + + "debugInjectTitle": "Тестовое уведомление", + "debugInjectSubtitle": "Отладка", + "debugInjectPackageLabel": "Пакет приложения", + "debugInjectTitleLabel": "Заголовок (необязательно)", + "debugInjectBodyLabel": "Текст уведомления", + "debugInjectSend": "Отправить в обработку", + "debugInjectSent": "Отправлено", + "debugInjectFillSample": "Пример: покупка" } diff --git a/lib/src/app/router/app_router.dart b/lib/src/app/router/app_router.dart index a5af90d..7b8f148 100644 --- a/lib/src/app/router/app_router.dart +++ b/lib/src/app/router/app_router.dart @@ -9,6 +9,11 @@ import '../../features/analytics/presentation/screens/analytics_screen.dart'; import '../../features/categories/presentation/screens/categories_list_screen.dart'; import '../../features/categories/presentation/screens/category_form_screen.dart'; import '../../features/home/presentation/screens/home_screen.dart'; +import '../../features/notification_parsing/presentation/screens/debug_inject_screen.dart'; +import '../../features/notification_parsing/presentation/screens/inbox_screen.dart'; +import '../../features/notification_parsing/presentation/screens/parsing_settings_screen.dart'; +import '../../features/notification_parsing/presentation/screens/rule_editor_screen.dart'; +import '../../features/notification_parsing/presentation/screens/rules_list_screen.dart'; import '../../features/profile/presentation/screens/profile_screen.dart'; import '../../features/transactions/presentation/screens/transaction_form_screen.dart'; import '../../features/user/application/active_user_controller.dart'; @@ -96,6 +101,34 @@ GoRouter appRouter(Ref ref) { AccountFormScreen(accountId: state.pathParameters['id']), ), ), + GoRoute( + path: AppRoutes.inbox, + pageBuilder: (context, state) => + _slideUpPage(state, const InboxScreen()), + ), + GoRoute( + path: AppRoutes.parsingSettings, + builder: (context, state) => const ParsingSettingsScreen(), + ), + GoRoute( + path: AppRoutes.parsingRules, + builder: (context, state) => const RulesListScreen(), + ), + // 'new' должен идти раньше ':id', иначе перехватится как id. + GoRoute( + path: AppRoutes.parsingRuleNew, + builder: (context, state) => + RuleEditorScreen(prefill: state.extra as RuleEditorPrefill?), + ), + GoRoute( + path: AppRoutes.parsingRuleEditPattern, + builder: (context, state) => + RuleEditorScreen(ruleId: state.pathParameters['id']), + ), + GoRoute( + path: AppRoutes.parsingDebugInject, + builder: (context, state) => const DebugInjectScreen(), + ), StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => AppScaffold( navigationShell: navigationShell, diff --git a/lib/src/app/router/app_routes.dart b/lib/src/app/router/app_routes.dart index 623cd39..d5af63f 100644 --- a/lib/src/app/router/app_routes.dart +++ b/lib/src/app/router/app_routes.dart @@ -19,4 +19,13 @@ class AppRoutes { static const accountNew = '/accounts/new'; static const accountEditPattern = '/accounts/edit/:id'; static String accountEdit(String id) => '/accounts/edit/$id'; + + // Notification parsing + static const inbox = '/inbox'; + static const parsingSettings = '/settings/parsing'; + static const parsingRules = '/settings/parsing/rules'; + static const parsingRuleNew = '/settings/parsing/rules/new'; + static const parsingRuleEditPattern = '/settings/parsing/rules/:id'; + static String parsingRuleEdit(String id) => '/settings/parsing/rules/$id'; + static const parsingDebugInject = '/settings/parsing/debug'; } diff --git a/lib/src/features/home/presentation/screens/home_screen.dart b/lib/src/features/home/presentation/screens/home_screen.dart index 8d5e6aa..4b917d4 100644 --- a/lib/src/features/home/presentation/screens/home_screen.dart +++ b/lib/src/features/home/presentation/screens/home_screen.dart @@ -11,6 +11,7 @@ import '../../../accounts/application/accounts_controller.dart'; import '../../../accounts/domain/entities/account.dart'; import '../../../categories/application/categories_controller.dart'; import '../../../categories/domain/entities/category.dart'; +import '../../../notification_parsing/application/parsing_worker.dart'; import '../../../transactions/domain/entities/transaction.dart'; import '../../../user/application/active_user_controller.dart'; import '../month_summary.dart'; @@ -58,6 +59,8 @@ class _HomeContent extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final l10n = context.l10n; final locale = Localizations.localeOf(context).toString(); + // Активируем ParsingWorker (keepAlive): слушает raw_messages.pending. + ref.watch(parsingWorkerProvider(userId)); final categories = ref.watch(categoriesStreamProvider(userId)).value ?? const []; diff --git a/lib/src/features/home/presentation/widgets/month_header.dart b/lib/src/features/home/presentation/widgets/month_header.dart index 743c704..6f76e63 100644 --- a/lib/src/features/home/presentation/widgets/month_header.dart +++ b/lib/src/features/home/presentation/widgets/month_header.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../app/l10n/l10n.dart'; +import '../../../../app/router/app_routes.dart'; import '../../../../app/theme/app_colors.dart'; +import '../../../notification_parsing/application/inbox_controller.dart'; +import '../../../user/application/active_user_controller.dart'; import '../state/selected_category_filter.dart'; class MonthHeader extends ConsumerWidget { @@ -82,18 +86,37 @@ class MonthHeader extends ConsumerWidget { icon: Icon(Icons.search, size: 20, color: p.ink2), ), const SizedBox(width: 4), - IconButton( - onPressed: () {}, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - icon: Icon(Icons.notifications_outlined, size: 20, color: p.ink2), - ), + _InboxButton(color: p.ink2), ], ), ); } } +/// Кнопка-бэдж Inbox: ✉N появляется при наличии непросмотренных уведомлений. +class _InboxButton extends ConsumerWidget { + const _InboxButton({required this.color}); + + final Color color; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userId = ref.watch(activeUserControllerProvider).value?.id; + final count = + userId == null ? 0 : (ref.watch(inboxCountProvider(userId)).value ?? 0); + + final button = IconButton( + onPressed: () => context.push(AppRoutes.inbox), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + icon: Icon(Icons.notifications_outlined, size: 20, color: color), + ); + + if (count == 0) return button; + return Badge.count(count: count, child: button); + } +} + class _NavButton extends StatelessWidget { const _NavButton({ required this.icon, diff --git a/lib/src/features/notification_parsing/application/inbox_controller.dart b/lib/src/features/notification_parsing/application/inbox_controller.dart new file mode 100644 index 0000000..91ce4c0 --- /dev/null +++ b/lib/src/features/notification_parsing/application/inbox_controller.dart @@ -0,0 +1,205 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../accounts/application/account_providers.dart'; +import '../../transactions/application/transactions_controller.dart'; +import '../data/parser/draft_codec.dart'; +import '../domain/entities/parse_draft.dart'; +import '../domain/entities/raw_message.dart'; +import '../domain/enums.dart'; +import 'notification_parsing_providers.dart'; + +part 'inbox_controller.g.dart'; + +/// Сообщения, ожидающие подтверждения в Inbox. +@riverpod +Stream> inboxMessages(Ref ref, String userId) => + ref.watch(rawMessagesRepositoryProvider).watchInbox(userId); + +/// Реактивный счётчик Inbox — для бэджа на Home. +@riverpod +Stream inboxCount(Ref ref, String userId) => + ref.watch(rawMessagesRepositoryProvider).watchInboxCount(userId); + +/// Действия над карточками Inbox (§9.1). Все мутации — явные тапы пользователя. +@Riverpod(keepAlive: true) +class InboxController extends _$InboxController { + @override + AsyncValue build() => const AsyncData(null); + + /// «Создать правило»: транзакция + parse_rule + удаление кандидата. + /// Все последующие похожие сообщения auto-apply молча. + Future createRule({ + required String userId, + required RawMessage message, + required ParseDraft draft, + required String accountId, + String? categoryId, + required String merchantCanonical, + required String pattern, + MatchMode matchMode = MatchMode.contains, + bool bindAccount = true, + }) async { + state = const AsyncLoading(); + try { + final tx = + await ref.read(transactionsControllerProvider.notifier).createTransaction( + userId: userId, + accountId: accountId, + categoryId: categoryId, + type: draft.type, + amount: draft.amount, + date: draft.dateTime ?? message.receivedAt, + merchant: merchantCanonical, + rawMessageId: message.id, + ); + + await ref.read(parseRulesRepositoryProvider).create( + userId: userId, + kind: ParseRuleKind.merchantToCategory, + matchMode: matchMode, + pattern: pattern, + merchantCanonical: merchantCanonical, + categoryId: categoryId, + ); + + if (draft.merchantRaw != null) { + await ref + .read(ruleCandidatesRepositoryProvider) + .deleteByRawValue(userId, draft.merchantRaw!); + } + + await ref.read(rawMessagesRepositoryProvider).linkTransaction(message.id, tx.id); + if (bindAccount) await _maybeBind(userId, message, draft, accountId); + state = const AsyncData(null); + } catch (e, st) { + state = AsyncError(e, st); + rethrow; + } + } + + /// «Подтвердить разово»: только транзакция (правило не создаём), + /// усиливаем кандидата для будущего предложения. + Future confirmOnce({ + required String userId, + required RawMessage message, + required ParseDraft draft, + required String accountId, + String? categoryId, + bool bindAccount = true, + }) async { + state = const AsyncLoading(); + try { + final tx = + await ref.read(transactionsControllerProvider.notifier).createTransaction( + userId: userId, + accountId: accountId, + categoryId: categoryId, + type: draft.type, + amount: draft.amount, + date: draft.dateTime ?? message.receivedAt, + merchant: draft.merchantCanonical ?? draft.merchantRaw, + rawMessageId: message.id, + ); + await ref.read(rawMessagesRepositoryProvider).linkTransaction(message.id, tx.id); + + if (draft.merchantRaw != null && categoryId != null) { + await ref.read(ruleCandidatesRepositoryProvider).observe( + userId: userId, + kind: ParseRuleKind.merchantToCategory, + rawValue: draft.merchantRaw!, + resolvedValue: categoryId, + ); + } + if (bindAccount) await _maybeBind(userId, message, draft, accountId); + state = const AsyncData(null); + } catch (e, st) { + state = AsyncError(e, st); + rethrow; + } + } + + /// «Игнорировать»: статус ignored, без правила. + Future ignore(RawMessage message) async { + await ref + .read(rawMessagesRepositoryProvider) + .updateStatus(message.id, RawMessageStatus.ignored); + } + + /// «Игнорировать» + правило-исключение (kind=ignore). + Future ignoreWithRule({ + required String userId, + required RawMessage message, + required String pattern, + MatchMode matchMode = MatchMode.contains, + }) async { + state = const AsyncLoading(); + try { + await ref.read(parseRulesRepositoryProvider).create( + userId: userId, + kind: ParseRuleKind.ignore, + matchMode: matchMode, + pattern: pattern, + ); + await ref + .read(rawMessagesRepositoryProvider) + .updateStatus(message.id, RawMessageStatus.ignored); + state = const AsyncData(null); + } catch (e, st) { + state = AsyncError(e, st); + rethrow; + } + } + + /// «Учесть все транзакции»: массовое «подтвердить разово». + Future applyAll(String userId) async { + final messages = + await ref.read(rawMessagesRepositoryProvider).watchInbox(userId).first; + final defaultAccount = + await ref.read(accountRepositoryProvider).watchDefault(userId).first; + for (final msg in messages) { + final bundle = decodeDraftBundle(msg.draftJson); + if (bundle == null) continue; + final accountId = bundle.draft.accountId ?? defaultAccount?.id; + if (accountId == null) continue; + await confirmOnce( + userId: userId, + message: msg, + draft: bundle.draft, + accountId: accountId, + categoryId: bundle.draft.categoryId ?? bundle.suggestion?.categoryId, + ); + } + } + + /// «Скрыть разово»: убрать из Inbox без создания транзакций. + Future hideAll(String userId) async { + final messages = + await ref.read(rawMessagesRepositoryProvider).watchInbox(userId).first; + final repo = ref.read(rawMessagesRepositoryProvider); + for (final msg in messages) { + await repo.updateStatus(msg.id, RawMessageStatus.ignored); + } + } + + /// Создаёт привязку «карта → счёт», если её ещё нет — чтобы следующее + /// сообщение того же мерчанта прошло gate молча (account score 100). + Future _maybeBind( + String userId, + RawMessage message, + ParseDraft draft, + String accountId, + ) async { + final last4 = draft.cardLast4; + if (last4 == null) return; + final bindings = ref.read(accountBindingsRepositoryProvider); + final existing = await bindings.findByPackageAndCard( + userId, message.packageName, last4); + if (existing != null) return; + await bindings.create( + userId: userId, + packageName: message.packageName, + cardLast4: last4, + accountId: accountId, + ); + } +} diff --git a/lib/src/features/notification_parsing/application/notification_parsing_providers.dart b/lib/src/features/notification_parsing/application/notification_parsing_providers.dart new file mode 100644 index 0000000..45a795e --- /dev/null +++ b/lib/src/features/notification_parsing/application/notification_parsing_providers.dart @@ -0,0 +1,33 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../../core/providers/database_provider.dart'; +import '../data/repositories/account_bindings_repository_impl.dart'; +import '../data/repositories/parse_rules_repository_impl.dart'; +import '../data/repositories/raw_messages_repository_impl.dart'; +import '../data/repositories/rule_candidates_repository_impl.dart'; +import '../domain/repositories/account_bindings_repository.dart'; +import '../domain/repositories/parse_rules_repository.dart'; +import '../domain/repositories/raw_messages_repository.dart'; +import '../domain/repositories/rule_candidates_repository.dart'; + +part 'notification_parsing_providers.g.dart'; + +/// keepAlive: репозитории держат открытые Drift-стримы — не пересоздавать. + +@Riverpod(keepAlive: true) +RawMessagesRepository rawMessagesRepository(Ref ref) => + RawMessagesRepositoryImpl(ref.watch(appDatabaseProvider).rawMessagesDao); + +@Riverpod(keepAlive: true) +ParseRulesRepository parseRulesRepository(Ref ref) => + ParseRulesRepositoryImpl(ref.watch(appDatabaseProvider).parseRulesDao); + +@Riverpod(keepAlive: true) +RuleCandidatesRepository ruleCandidatesRepository(Ref ref) => + RuleCandidatesRepositoryImpl( + ref.watch(appDatabaseProvider).ruleCandidatesDao); + +@Riverpod(keepAlive: true) +AccountBindingsRepository accountBindingsRepository(Ref ref) => + AccountBindingsRepositoryImpl( + ref.watch(appDatabaseProvider).accountBindingsDao); diff --git a/lib/src/features/notification_parsing/application/parsing_settings_controller.dart b/lib/src/features/notification_parsing/application/parsing_settings_controller.dart new file mode 100644 index 0000000..e949619 --- /dev/null +++ b/lib/src/features/notification_parsing/application/parsing_settings_controller.dart @@ -0,0 +1,58 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../../core/providers/database_provider.dart'; + +part 'parsing_settings_controller.g.dart'; + +/// Настройки фичи парсинга (Phase 1) — хранятся в key-value `app_preferences`, +/// без изменения схемы. +class ParsingSettings { + const ParsingSettings({required this.enabled, required this.strictness}); + + /// Распознавать уведомления (мастер-тумблер всей фичи). + final bool enabled; + + /// Порог строгости авто-добавления для прочих полей (§8.7): 75 / 85 / 95. + final int strictness; + + ParsingSettings copyWith({bool? enabled, int? strictness}) => ParsingSettings( + enabled: enabled ?? this.enabled, + strictness: strictness ?? this.strictness, + ); +} + +const _kEnabled = 'parsing_enabled'; +const _kStrictness = 'parsing_auto_apply_strictness'; +const _defaultSettings = ParsingSettings(enabled: true, strictness: 85); + +@Riverpod(keepAlive: true) +class ParsingSettingsController extends _$ParsingSettingsController { + @override + Future build() async { + final dao = ref.watch(appDatabaseProvider).settingsDao; + final enabledStr = await dao.getPreference(_kEnabled); + final strictnessStr = await dao.getPreference(_kStrictness); + return ParsingSettings( + enabled: enabledStr == null ? _defaultSettings.enabled : enabledStr == 'true', + strictness: int.tryParse(strictnessStr ?? '') ?? _defaultSettings.strictness, + ); + } + + Future setEnabled(bool value) async { + await ref + .read(appDatabaseProvider) + .settingsDao + .setPreference(_kEnabled, '$value'); + final current = state.value ?? _defaultSettings; + state = AsyncData(current.copyWith(enabled: value)); + } + + Future setStrictness(int value) async { + await ref + .read(appDatabaseProvider) + .settingsDao + .setPreference(_kStrictness, '$value'); + final current = state.value ?? _defaultSettings; + state = AsyncData(current.copyWith(strictness: value)); + } +} diff --git a/lib/src/features/notification_parsing/application/parsing_worker.dart b/lib/src/features/notification_parsing/application/parsing_worker.dart new file mode 100644 index 0000000..34dcaa7 --- /dev/null +++ b/lib/src/features/notification_parsing/application/parsing_worker.dart @@ -0,0 +1,221 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../categories/application/category_providers.dart'; +import '../../transactions/application/transactions_controller.dart'; +import '../data/parser/account_resolver.dart'; +import '../data/parser/confidence_scorer.dart'; +import '../data/parser/decision_gate.dart'; +import '../data/parser/draft_codec.dart'; +import '../data/parser/regex_parser.dart'; +import '../data/parser/rule_lookup.dart'; +import '../data/parser/rule_suggester.dart'; +import '../domain/entities/parse_draft.dart'; +import '../domain/entities/raw_message.dart'; +import '../domain/entities/rule_candidate.dart'; +import '../domain/entities/rule_suggestion.dart'; +import '../domain/enums.dart'; +import 'notification_parsing_providers.dart'; +import 'parsing_settings_controller.dart'; + +part 'parsing_worker.g.dart'; + +/// Поток необработанных сообщений — вход воркера. +@riverpod +Stream> pendingMessages(Ref ref, String userId) => + ref.watch(rawMessagesRepositoryProvider).watchPending(userId); + +/// ParsingWorker (§5): слушает `raw_messages.pending` и прогоняет pipeline +/// regex → resolver → rule_lookup → suggester → scorer → gate по одному. +/// +/// Идемпотентен: при возврате сообщения в `pending` перепарсивается. +/// Провайдер `keepAlive` — активируется чтением из HomeScreen. +@Riverpod(keepAlive: true) +class ParsingWorker extends _$ParsingWorker { + final Set _inFlight = {}; + + @override + void build(String userId) { + final sub = ref.listen( + pendingMessagesProvider(userId), + (_, next) { + final list = next.value; + if (list != null && list.isNotEmpty) { + _drain(userId, list); + } + }, + fireImmediately: true, + ); + ref.onDispose(sub.close); + } + + Future _drain(String userId, List pending) async { + for (final msg in pending) { + if (_inFlight.contains(msg.id)) continue; + _inFlight.add(msg.id); + final repo = ref.read(rawMessagesRepositoryProvider); + try { + await repo.incrementParseAttempts(msg.id); + await _process(userId, msg); + } catch (e) { + await repo.updateAfterParse( + id: msg.id, + status: RawMessageStatus.failed, + ); + } finally { + _inFlight.remove(msg.id); + } + } + } + + Future _process(String userId, RawMessage msg) async { + final repo = ref.read(rawMessagesRepositoryProvider); + + final settings = await ref.read(parsingSettingsControllerProvider.future); + if (!settings.enabled) return; // фича выключена — оставляем pending. + + // 1. Regex (Phase 1 — без AI fallback). + final parsed = const RegexParser().parse(msg); + if (parsed == null) { + // Не распознано: баланс/реклама → ignored, иначе → Inbox (вручную). + if (looksLikeNonTransaction(msg.body)) { + await repo.updateStatus(msg.id, RawMessageStatus.ignored); + } else { + await repo.updateAfterParse(id: msg.id, status: RawMessageStatus.inbox); + } + return; + } + + // 3. Разрешение счёта. + final resolver = + AccountResolver(ref.read(accountBindingsRepositoryProvider)); + final resolution = await resolver.resolve( + userId: userId, + packageName: msg.packageName, + cardLast4: parsed.cardLast4, + phone: parsed.counterpartyPhone, + ); + var draft = parsed.copyWith(accountId: resolution.accountId); + + // 4. Правила: исключение → ignored; иначе ищем merchantToCategory. + final rules = + await ref.read(parseRulesRepositoryProvider).getEnabledByUser(userId); + if (findIgnoreRule(rules, body: msg.body, merchantRaw: draft.merchantRaw) != + null) { + await repo.updateStatus(msg.id, RawMessageStatus.ignored); + return; + } + final rule = + findMerchantRule(rules, body: msg.body, merchantRaw: draft.merchantRaw); + + RuleCandidate? candidate; + if (draft.merchantRaw != null) { + candidate = await ref.read(ruleCandidatesRepositoryProvider).findByRawValue( + userId, + ParseRuleKind.merchantToCategory, + draft.merchantRaw!, + ); + } + + // Применяем действие правила к draft. + if (rule != null) { + draft = draft.copyWith( + merchantCanonical: rule.merchantCanonical, + categoryId: rule.categoryId, + accountId: draft.accountId ?? rule.accountId, + ); + } + + // Предложение правила для Inbox (только для незнакомого мерчанта). + RuleSuggestion? suggestion; + if (rule == null && draft.merchantRaw != null) { + suggestion = + suggestRule(merchantRaw: draft.merchantRaw!, categoryCandidate: candidate); + if (suggestion.categoryId != null) { + final cat = await ref + .read(categoryRepositoryProvider) + .findById(suggestion.categoryId!); + suggestion = suggestion.copyWith(categoryName: cat?.name); + } + } + + // 6. Confidence. + final scores = scoreDraft( + draft: draft, + message: msg, + accountScore: resolution.score, + merchantRule: rule, + merchantCandidate: candidate, + categoryCandidate: candidate, + ); + + // 8. Gate. + final decision = decide( + merchantRule: rule, + sanityPassed: true, + scores: scores, + strictness: settings.strictness, + amountMinor: draft.amount, + hasAccount: draft.accountId != null, + ); + + if (decision == GateDecision.autoApply) { + await _autoApply(userId, msg, draft, rule!, scores, resolution.bindingId); + } else { + await repo.updateAfterParse( + id: msg.id, + status: RawMessageStatus.inbox, + draftJson: encodeDraftBundle(draft, suggestion), + confidenceAmount: scores.amount, + confidenceAccount: scores.account, + confidenceType: scores.type, + confidenceMerchant: scores.merchant, + confidenceCategory: scores.category, + ); + } + } + + Future _autoApply( + String userId, + RawMessage msg, + ParseDraft draft, + rule, + FieldScores scores, + String? bindingId, + ) async { + final repo = ref.read(rawMessagesRepositoryProvider); + final tx = + await ref.read(transactionsControllerProvider.notifier).createTransaction( + userId: userId, + accountId: draft.accountId!, + categoryId: draft.categoryId, + type: draft.type, + amount: draft.amount, + date: draft.dateTime ?? msg.receivedAt, + merchant: draft.merchantCanonical ?? draft.merchantRaw, + rawMessageId: msg.id, + autoApplied: true, + appliedByRuleId: rule.id, + ); + + await repo.updateAfterParse( + id: msg.id, + status: RawMessageStatus.applied, + draftJson: encodeDraftBundle(draft, null), + confidenceAmount: scores.amount, + confidenceAccount: scores.account, + confidenceType: scores.type, + confidenceMerchant: scores.merchant, + confidenceCategory: scores.category, + ); + await repo.linkTransaction(msg.id, tx.id); + + await ref + .read(parseRulesRepositoryProvider) + .incrementMatchCount(rule.id, DateTime.now()); + if (bindingId != null) { + await ref + .read(accountBindingsRepositoryProvider) + .incrementMatchCount(bindingId); + } + } +} diff --git a/lib/src/features/notification_parsing/application/rules_controller.dart b/lib/src/features/notification_parsing/application/rules_controller.dart new file mode 100644 index 0000000..fc98967 --- /dev/null +++ b/lib/src/features/notification_parsing/application/rules_controller.dart @@ -0,0 +1,90 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../data/parser/rule_lookup.dart'; +import '../domain/entities/parse_rule.dart'; +import '../domain/entities/raw_message.dart'; +import '../domain/enums.dart'; +import 'notification_parsing_providers.dart'; + +part 'rules_controller.g.dart'; + +/// Все правила пользователя — для экрана «Правила парсинга». +@riverpod +Stream> parseRulesList(Ref ref, String userId) => + ref.watch(parseRulesRepositoryProvider).watchByUser(userId); + +/// Одно правило по id — для редактора. +@riverpod +Future parseRuleById(Ref ref, String id) => + ref.watch(parseRulesRepositoryProvider).findById(id); + +/// Live-превью (§9.3): сообщения за 30 дней, совпадающие с паттерном. +@riverpod +Future> ruleMatchPreview( + Ref ref, + String userId, + String pattern, + MatchMode mode, +) async { + if (pattern.trim().isEmpty) return const []; + final since = DateTime.now().subtract(const Duration(days: 30)); + final messages = + await ref.watch(rawMessagesRepositoryProvider).recentByUser(userId, since); + return messages + .where((m) => patternMatches(pattern, mode, body: m.body)) + .toList(); +} + +/// CRUD над правилами парсинга. +@Riverpod(keepAlive: true) +class RulesController extends _$RulesController { + @override + AsyncValue build() => const AsyncData(null); + + Future create({ + required String userId, + required ParseRuleKind kind, + required MatchMode matchMode, + required String pattern, + int priority = 0, + String? merchantCanonical, + String? categoryId, + String? accountId, + }) async { + state = const AsyncLoading(); + try { + final rule = await ref.read(parseRulesRepositoryProvider).create( + userId: userId, + kind: kind, + matchMode: matchMode, + pattern: pattern, + priority: priority, + merchantCanonical: merchantCanonical, + categoryId: categoryId, + accountId: accountId, + ); + state = const AsyncData(null); + return rule; + } catch (e, st) { + state = AsyncError(e, st); + rethrow; + } + } + + Future update(ParseRule rule) async { + state = const AsyncLoading(); + try { + await ref.read(parseRulesRepositoryProvider).update(rule); + state = const AsyncData(null); + } catch (e, st) { + state = AsyncError(e, st); + rethrow; + } + } + + Future setEnabled(String id, {required bool enabled}) => + ref.read(parseRulesRepositoryProvider).setEnabled(id, enabled: enabled); + + Future delete(String id) => + ref.read(parseRulesRepositoryProvider).deleteById(id); +} diff --git a/lib/src/features/notification_parsing/data/bank_templates/bank_templates_catalog.dart b/lib/src/features/notification_parsing/data/bank_templates/bank_templates_catalog.dart new file mode 100644 index 0000000..441e192 --- /dev/null +++ b/lib/src/features/notification_parsing/data/bank_templates/bank_templates_catalog.dart @@ -0,0 +1,63 @@ +import '../../../../core/database/converters/enum_converters.dart'; +import '../../domain/entities/bank_template.dart'; + +/// Встроенный набор RU-шаблонов (regex first, §6). +/// +/// Шаблоны универсальны (не привязаны к конкретным банкам) и покрывают +/// распространённые форматы РФ: покупка/списание с карты, зачисление, +/// СБП-перевод по телефону, возврат. Если ни один не совпал — этап 2 (AI, +/// Phase 2); в Phase 1 это `parsed_partial` → Inbox. +/// +/// Группы извлечения нумерованные (`$1`, `$2`, …) — соответствуют capture- +/// группам в [BankTemplate.pattern]. Матч регистронезависимый. +const List bankTemplatesCatalog = [ + // 1. Покупка: сумма → ₽ → *last4 → мерчант (Сбер-подобный формат). + // «Покупка 1240р Карта *3456 PYATEROCHKA 14:05 Баланс: 5000р» + BankTemplate( + key: 'purchase_amount_card_merchant_ru', + pattern: + r'(?:Покупка|Оплата|Списание)\s+(\d[\d\s.,  ]*)\s*(?:₽|руб\.?|р\.?|RUB)[\s\S]*?\*(\d{4})\s+([^\d\n][^\n]*?)(?=\s+\d{1,2}[:.]\d{2}|\s+Баланс|\s+Доступно|[.;]|$)', + extract: {'amount': r'$1', 'cardLast4': r'$2', 'merchant': r'$3'}, + type: TransactionType.expense, + ), + + // 2. Покупка: *last4 → сумма ₽ → мерчант (Тинькофф-подобный формат). + // «Покупка. Карта *3456. 1 240 ₽. Пятёрочка. Доступно 5 000 ₽» + BankTemplate( + key: 'purchase_card_amount_merchant_ru', + pattern: + r'(?:Покупка|Оплата|Списание)[\s\S]*?\*(\d{4})[\s\S]*?(\d[\d\s.,  ]*)\s*(?:₽|руб\.?|RUB)\.?\s+([^\d\n][^\n.]*?)(?=[.]|\s+Доступно|\s+Баланс|$)', + extract: {'cardLast4': r'$1', 'amount': r'$2', 'merchant': r'$3'}, + type: TransactionType.expense, + ), + + // 3. Зачисление / пополнение / поступление. + // «Зачисление 5000 ₽ Карта *3456 ...» + BankTemplate( + key: 'income_card_ru', + pattern: + r'(?:Зачисление|Пополнение|Поступление)\s+(\d[\d\s.,  ]*)\s*(?:₽|руб\.?|RUB)[\s\S]*?\*(\d{4})', + extract: {'amount': r'$1', 'cardLast4': r'$2'}, + type: TransactionType.income, + ), + + // 4. СБП-перевод по телефону. + // «Перевод 1000 ₽ на +79991234567 ...» + BankTemplate( + key: 'sbp_phone_ru', + pattern: + r'(?:Перевод|СБП|Отправлен[оы]?)[\s\S]*?(\d[\d\s.,  ]*)\s*(?:₽|руб\.?|RUB)[\s\S]*?(\+7\d{10})', + extract: {'amount': r'$1', 'phone': r'$2'}, + type: TransactionType.expense, + ), + + // 5. Возврат покупки. + // «Возврат 1240 ₽ Карта *3456 ...» + BankTemplate( + key: 'refund_card_ru', + pattern: + r'(?:Возврат|Refund)\s+(\d[\d\s.,  ]*)\s*(?:₽|руб\.?|RUB)[\s\S]*?\*(\d{4})', + extract: {'amount': r'$1', 'cardLast4': r'$2'}, + type: TransactionType.income, + ), +]; diff --git a/lib/src/features/notification_parsing/data/drift/daos/raw_messages_dao.dart b/lib/src/features/notification_parsing/data/drift/daos/raw_messages_dao.dart index 3d61944..84f3b3a 100644 --- a/lib/src/features/notification_parsing/data/drift/daos/raw_messages_dao.dart +++ b/lib/src/features/notification_parsing/data/drift/daos/raw_messages_dao.dart @@ -48,6 +48,17 @@ class RawMessagesDao extends DatabaseAccessor (select(rawMessagesTable)..where((t) => t.id.equals(id))) .getSingleOrNull(); + /// Последние сообщения пользователя начиная с [since] — для live-превью + /// в редакторе правил (матч паттерна по реальным сообщениям). + Future> getRecentByUser( + String userId, DateTime since) => + (select(rawMessagesTable) + ..where((t) => + t.userId.equals(userId) & + t.receivedAt.isBiggerOrEqualValue(since)) + ..orderBy([(t) => OrderingTerm.desc(t.receivedAt)])) + .get(); + /// Поиск по dedupHash для идемпотентной вставки. Future findByDedupHash( String userId, String dedupHash) => diff --git a/lib/src/features/notification_parsing/data/mappers/account_binding_mapper.dart b/lib/src/features/notification_parsing/data/mappers/account_binding_mapper.dart new file mode 100644 index 0000000..1956a42 --- /dev/null +++ b/lib/src/features/notification_parsing/data/mappers/account_binding_mapper.dart @@ -0,0 +1,17 @@ +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/account_binding.dart'; + +/// Маппер: строка Drift → доменная сущность [AccountBinding]. +extension AccountBindingMapper on AccountBindingsTableData { + AccountBinding toDomain() => AccountBinding( + id: id, + userId: userId, + packageName: packageName, + bankKey: bankKey, + cardLast4: cardLast4, + phone: phone, + accountId: accountId, + matchCount: matchCount, + createdAt: createdAt, + ); +} diff --git a/lib/src/features/notification_parsing/data/mappers/parse_rule_mapper.dart b/lib/src/features/notification_parsing/data/mappers/parse_rule_mapper.dart new file mode 100644 index 0000000..b414372 --- /dev/null +++ b/lib/src/features/notification_parsing/data/mappers/parse_rule_mapper.dart @@ -0,0 +1,22 @@ +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/parse_rule.dart'; + +/// Маппер: строка Drift → доменная сущность [ParseRule]. +extension ParseRuleMapper on ParseRulesTableData { + ParseRule toDomain() => ParseRule( + id: id, + userId: userId, + kind: kind, + matchMode: matchMode, + pattern: pattern, + priority: priority, + matchCount: matchCount, + weight: weight, + lastMatchAt: lastMatchAt, + merchantCanonical: merchantCanonical, + categoryId: categoryId, + accountId: accountId, + enabled: enabled, + createdAt: createdAt, + ); +} diff --git a/lib/src/features/notification_parsing/data/mappers/raw_message_mapper.dart b/lib/src/features/notification_parsing/data/mappers/raw_message_mapper.dart new file mode 100644 index 0000000..e129d9a --- /dev/null +++ b/lib/src/features/notification_parsing/data/mappers/raw_message_mapper.dart @@ -0,0 +1,26 @@ +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/raw_message.dart'; + +/// Маппер: строка Drift → доменная сущность [RawMessage]. +extension RawMessageMapper on RawMessagesTableData { + RawMessage toDomain() => RawMessage( + id: id, + userId: userId, + packageName: packageName, + title: title, + body: body, + receivedAt: receivedAt, + dedupHash: dedupHash, + status: status, + parseAttemptCount: parseAttemptCount, + lastParseError: lastParseError, + draftJson: draftJson, + confidenceAmount: confidenceAmount, + confidenceAccount: confidenceAccount, + confidenceType: confidenceType, + confidenceMerchant: confidenceMerchant, + confidenceCategory: confidenceCategory, + transactionId: transactionId, + createdAt: createdAt, + ); +} diff --git a/lib/src/features/notification_parsing/data/mappers/rule_candidate_mapper.dart b/lib/src/features/notification_parsing/data/mappers/rule_candidate_mapper.dart new file mode 100644 index 0000000..18285a0 --- /dev/null +++ b/lib/src/features/notification_parsing/data/mappers/rule_candidate_mapper.dart @@ -0,0 +1,16 @@ +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/rule_candidate.dart'; + +/// Маппер: строка Drift → доменная сущность [RuleCandidate]. +extension RuleCandidateMapper on RuleCandidatesTableData { + RuleCandidate toDomain() => RuleCandidate( + id: id, + userId: userId, + kind: kind, + rawValue: rawValue, + resolvedValue: resolvedValue, + seenCount: seenCount, + firstSeenAt: firstSeenAt, + lastSeenAt: lastSeenAt, + ); +} diff --git a/lib/src/features/notification_parsing/data/parser/account_resolver.dart b/lib/src/features/notification_parsing/data/parser/account_resolver.dart new file mode 100644 index 0000000..1320f07 --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/account_resolver.dart @@ -0,0 +1,64 @@ +import '../../domain/repositories/account_bindings_repository.dart'; + +/// Результат разрешения счёта из уведомления (шаг 3 pipeline). +class AccountResolution { + const AccountResolution({ + required this.accountId, + required this.score, + this.bindingId, + }); + + /// Разрешённый счёт (null, если binding не нашёлся). + final String? accountId; + + /// Confidence по счёту 0..100 (§8.2). + final int score; + + /// id сработавшей привязки — для инкремента matchCount. + final String? bindingId; +} + +/// Разрешает `accountId` по `account_bindings` (карта/телефон → счёт). +class AccountResolver { + const AccountResolver(this._bindings); + + final AccountBindingsRepository _bindings; + + Future resolve({ + required String userId, + required String packageName, + String? cardLast4, + String? phone, + }) async { + if (cardLast4 != null) { + final b = + await _bindings.findByPackageAndCard(userId, packageName, cardLast4); + if (b != null) { + return AccountResolution( + accountId: b.accountId, score: 100, bindingId: b.id); + } + } + + if (phone != null) { + final b = await _bindings.findByPhone(userId, phone); + if (b != null) { + return AccountResolution( + accountId: b.accountId, score: 100, bindingId: b.id); + } + } + + final byPkg = await _bindings.findByPackageName(userId, packageName); + if (byPkg.length == 1) { + // Один счёт для этого банка — высокая уверенность (§8.2). + return AccountResolution( + accountId: byPkg.first.accountId, score: 75, bindingId: byPkg.first.id); + } + if (byPkg.length > 1) { + // Несколько счетов — эвристика «первый», низкая уверенность. + return AccountResolution( + accountId: byPkg.first.accountId, score: 45, bindingId: byPkg.first.id); + } + + return const AccountResolution(accountId: null, score: 15); + } +} diff --git a/lib/src/features/notification_parsing/data/parser/confidence_scorer.dart b/lib/src/features/notification_parsing/data/parser/confidence_scorer.dart new file mode 100644 index 0000000..1b218ed --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/confidence_scorer.dart @@ -0,0 +1,117 @@ +import '../../domain/entities/parse_draft.dart'; +import '../../domain/entities/parse_rule.dart'; +import '../../domain/entities/raw_message.dart'; +import '../../domain/entities/rule_candidate.dart'; + +/// Пять per-field оценок уверенности (0..100), сохраняются в `raw_messages`. +class FieldScores { + const FieldScores({ + required this.amount, + required this.account, + required this.type, + required this.merchant, + required this.category, + }); + + final int amount; + final int account; + final int type; + final int merchant; + final int category; + + /// Минимум по «прочим полям» — это и есть вход в gate (§8.7). + int get otherFieldsMin => + [amount, account, type].reduce((a, b) => a < b ? a : b); +} + +/// Считает 5 per-field оценок (§8.1–8.6). +/// +/// Confidence играет вспомогательную роль: судьбу мерчанта решает наличие +/// правила (§5), а эти оценки гейтят прочие поля и дают подсветку «?». +/// +/// В Phase 1 источник draft — только regex (AI нет), поэтому amount/type при +/// успешном шаблоне = 100; account приходит из resolver ([accountScore]). +FieldScores scoreDraft({ + required ParseDraft draft, + required RawMessage message, + required int accountScore, + ParseRule? merchantRule, + RuleCandidate? merchantCandidate, + RuleCandidate? categoryCandidate, +}) { + // Amount: regex выдал единственную сумму → 100 (§8.1). + var amount = 100; + + // Type: bank template явно указал тип → 100 (§8.3). + const type = 100; + + // Merchant (§8.4). + var merchant = _scoreMerchant(draft, merchantRule, merchantCandidate); + + // Category (§8.5). + var category = + _scoreCategory(draft, merchantRule, categoryCandidate); + + // ── Sanity checks / capping (§8.6) — обнуляют независимо от источника ── + final raw = draft.merchantRaw ?? ''; + if (raw.length < 5) merchant = _capAt(merchant, 30); + if (raw.isNotEmpty && RegExp(r'^[\d\s*+\-]+$').hasMatch(raw)) { + merchant = _capAt(merchant, 20); + } + if (draft.currency.isEmpty) amount = _capAt(amount, 40); + if (message.body.length < 20) { + amount = _capAt(amount, 50); + merchant = _capAt(merchant, 50); + category = _capAt(category, 50); + } + + // Жёсткое правило: категория не увереннее мерчанта (§8.5). + category = category > merchant ? merchant : category; + + return FieldScores( + amount: amount, + account: accountScore, + type: type, + merchant: merchant, + category: category, + ); +} + +int _scoreMerchant( + ParseDraft draft, + ParseRule? rule, + RuleCandidate? candidate, +) { + if (rule != null) return 100; // сработало активное правило + if (candidate != null) { + return candidate.seenCount >= 3 ? 80 : 60; + } + final raw = draft.merchantRaw ?? ''; + // «Прошёл sanity» здесь грубо: непустое осмысленное имя. + return raw.length >= 5 ? 40 : 15; +} + +int _scoreCategory( + ParseDraft draft, + ParseRule? rule, + RuleCandidate? candidate, +) { + if (rule?.categoryId != null) return 100; + if (candidate != null) { + return candidate.seenCount >= 3 ? 85 : 65; + } + // Новый мерчант без AI-подсказки (Phase 1) — слабо. + return 25; +} + +int _capAt(int value, int cap) => value > cap ? cap : value; + +/// Сообщение похоже на «не транзакцию» (баланс/реклама) — §8.6 последнее правило. +bool looksLikeNonTransaction(String body) { + final lower = body.toLowerCase(); + final hasBalanceWord = lower.contains('баланс') || lower.contains('доступно'); + final hasTxKeyword = RegExp( + r'покупка|оплата|списание|зачисление|пополнение|поступление|перевод|возврат') + .hasMatch(lower); + return hasBalanceWord && !hasTxKeyword; +} diff --git a/lib/src/features/notification_parsing/data/parser/decision_gate.dart b/lib/src/features/notification_parsing/data/parser/decision_gate.dart new file mode 100644 index 0000000..76470f5 --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/decision_gate.dart @@ -0,0 +1,35 @@ +import '../../domain/entities/parse_rule.dart'; +import 'confidence_scorer.dart'; + +/// Решение gate: молча в ленту или в Inbox. +enum GateDecision { autoApply, inbox } + +/// Очень крупная сумма (₽) — всегда Inbox, защита от галлюцинаций (§15). +/// Минорные единицы: 100 000 ₽ = 10 000 000 копеек. +const int _hugeAmountMinor = 100000 * 100; + +/// Decision gate (§8.7). +/// +/// Молча в ленту только если есть подтверждённое правило для мерчанта, +/// sanity пройден, и min(сумма, счёт, тип) ≥ порога строгости. +/// Порог ([strictness]) применяется к прочим полям, НЕ к мерчанту. +GateDecision decide({ + required ParseRule? merchantRule, + required bool sanityPassed, + required FieldScores scores, + required int strictness, + required int amountMinor, + required bool hasAccount, +}) { + if (amountMinor > _hugeAmountMinor) return GateDecision.inbox; + + // Без разрешённого счёта нечем создать транзакцию молча. + if (!hasAccount) return GateDecision.inbox; + + if (merchantRule != null && + sanityPassed && + scores.otherFieldsMin >= strictness) { + return GateDecision.autoApply; + } + return GateDecision.inbox; +} diff --git a/lib/src/features/notification_parsing/data/parser/dedup.dart b/lib/src/features/notification_parsing/data/parser/dedup.dart new file mode 100644 index 0000000..e357360 --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/dedup.dart @@ -0,0 +1,28 @@ +/// Детерминированный хэш для дедупликации уведомлений (§15). +/// +/// `String.hashCode` в Dart рандомизируется на каждый запуск изолята, поэтому +/// для идемпотентности «переживающей» перезагрузку устройства используем +/// стабильный FNV-1a (64-bit) и кодируем в hex. Без внешних зависимостей. +/// +/// Хэш считается по `packageName + '\n' + body` — **без времени**, чтобы +/// повторная доставка того же сообщения схлопывалась в одну запись. +String computeDedupHash(String packageName, String body) { + final input = '$packageName\n$body'; + return _fnv1a64(input); +} + +const int _fnvOffset = 0xcbf29ce484222325; +const int _fnvPrime = 0x100000001b3; + +String _fnv1a64(String s) { + // BigInt-free 64-bit FNV-1a поверх unicode code units. Dart int — 64-bit + // на mobile/desktop; переполнение оборачивается, что для хэша приемлемо. + var hash = _fnvOffset; + final units = s.codeUnits; + for (var i = 0; i < units.length; i++) { + hash ^= units[i]; + hash *= _fnvPrime; + } + // Приводим к беззнаковому hex фиксированной ширины. + return hash.toUnsigned(64).toRadixString(16).padLeft(16, '0'); +} diff --git a/lib/src/features/notification_parsing/data/parser/draft_codec.dart b/lib/src/features/notification_parsing/data/parser/draft_codec.dart new file mode 100644 index 0000000..e70674e --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/draft_codec.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import '../../../../core/database/converters/enum_converters.dart'; +import '../../domain/entities/parse_draft.dart'; +import '../../domain/entities/rule_suggestion.dart'; +import '../../domain/enums.dart'; + +/// Сериализация связки `ParseDraft` + `RuleSuggestion` в строку для +/// `RawMessage.draftJson`. Проект не использует json_serializable — +/// кодируем вручную (см. CLAUDE.md). +/// +/// Связка кешируется при отправке сообщения в Inbox, чтобы экран мог +/// восстановить предложение без повторного парсинга. +class DraftBundle { + const DraftBundle({required this.draft, this.suggestion}); + + final ParseDraft draft; + final RuleSuggestion? suggestion; +} + +String encodeDraftBundle(ParseDraft draft, RuleSuggestion? suggestion) { + return jsonEncode({ + 'draft': _draftToJson(draft), + if (suggestion != null) 'suggestion': _suggestionToJson(suggestion), + }); +} + +DraftBundle? decodeDraftBundle(String? json) { + if (json == null || json.isEmpty) return null; + final map = jsonDecode(json) as Map; + final draftMap = map['draft'] as Map?; + if (draftMap == null) return null; + final suggMap = map['suggestion'] as Map?; + return DraftBundle( + draft: _draftFromJson(draftMap), + suggestion: suggMap == null ? null : _suggestionFromJson(suggMap), + ); +} + +Map _draftToJson(ParseDraft d) => { + 'rawMessageId': d.rawMessageId, + 'type': d.type.name, + 'amount': d.amount, + 'currency': d.currency, + 'cardLast4': d.cardLast4, + 'merchantRaw': d.merchantRaw, + 'counterpartyName': d.counterpartyName, + 'counterpartyPhone': d.counterpartyPhone, + 'dateTime': d.dateTime?.toIso8601String(), + 'kind': d.kind?.name, + 'categorySuggestion': d.categorySuggestion, + 'accountId': d.accountId, + 'merchantCanonical': d.merchantCanonical, + 'categoryId': d.categoryId, + 'transferToAccountId': d.transferToAccountId, + 'source': d.source.name, + }; + +ParseDraft _draftFromJson(Map m) => ParseDraft( + rawMessageId: m['rawMessageId'] as String, + type: TransactionType.values.byName(m['type'] as String), + amount: m['amount'] as int, + currency: (m['currency'] as String?) ?? 'RUB', + cardLast4: m['cardLast4'] as String?, + merchantRaw: m['merchantRaw'] as String?, + counterpartyName: m['counterpartyName'] as String?, + counterpartyPhone: m['counterpartyPhone'] as String?, + dateTime: m['dateTime'] == null + ? null + : DateTime.parse(m['dateTime'] as String), + kind: m['kind'] == null ? null : TxKind.values.byName(m['kind'] as String), + categorySuggestion: m['categorySuggestion'] as String?, + accountId: m['accountId'] as String?, + merchantCanonical: m['merchantCanonical'] as String?, + categoryId: m['categoryId'] as String?, + transferToAccountId: m['transferToAccountId'] as String?, + source: ParseSource.values.byName(m['source'] as String), + ); + +Map _suggestionToJson(RuleSuggestion s) => { + 'merchantRaw': s.merchantRaw, + 'merchantCanonical': s.merchantCanonical, + 'categoryId': s.categoryId, + 'categoryName': s.categoryName, + 'accountId': s.accountId, + }; + +RuleSuggestion _suggestionFromJson(Map m) => RuleSuggestion( + merchantRaw: m['merchantRaw'] as String, + merchantCanonical: m['merchantCanonical'] as String, + categoryId: m['categoryId'] as String?, + categoryName: m['categoryName'] as String?, + accountId: m['accountId'] as String?, + ); diff --git a/lib/src/features/notification_parsing/data/parser/merchant_normalizer.dart b/lib/src/features/notification_parsing/data/parser/merchant_normalizer.dart new file mode 100644 index 0000000..c7eee3c --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/merchant_normalizer.dart @@ -0,0 +1,17 @@ +/// Нормализует сырое имя мерчанта из уведомления в canonical-форму для +/// предложения правила. +/// +/// `«WBSPB*MOSCOW»` → `«WBSPB»`, `«PYATEROCHKA 2351»` → `«PYATEROCHKA»`. +/// Отрезает хвост после `*`, завершающие цифры и схлопывает пробелы. +String normalizeMerchant(String raw) { + var s = raw.trim(); + + final star = s.indexOf('*'); + if (star > 0) s = s.substring(0, star); + + s = s.replaceAll(RegExp(r'\s+'), ' ').trim(); + // Убираем завершающие цифры/коды торговой точки. + s = s.replaceAll(RegExp(r'[\s\d]+$'), '').trim(); + + return s.isEmpty ? raw.trim() : s; +} diff --git a/lib/src/features/notification_parsing/data/parser/regex_parser.dart b/lib/src/features/notification_parsing/data/parser/regex_parser.dart new file mode 100644 index 0000000..b71c64a --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/regex_parser.dart @@ -0,0 +1,95 @@ +import '../../../../core/database/converters/enum_converters.dart'; +import '../../domain/entities/bank_template.dart'; +import '../../domain/entities/parse_draft.dart'; +import '../../domain/entities/raw_message.dart'; +import '../../domain/enums.dart'; +import '../bank_templates/bank_templates_catalog.dart'; + +/// Этап 1 pipeline: разбор тела уведомления встроенными regex-шаблонами. +/// +/// Возвращает `null`, если ни один шаблон не совпал — тогда pipeline уходит +/// на AI fallback (Phase 2) или в `parsed_partial`. +class RegexParser { + const RegexParser([this.templates = bankTemplatesCatalog]); + + final List templates; + + ParseDraft? parse(RawMessage msg) { + for (final t in templates) { + if (!t.enabled) continue; + final re = RegExp(t.pattern, caseSensitive: false); + final m = re.firstMatch(msg.body); + if (m == null) continue; + + final amountStr = _group(t.extract['amount'], m); + final amount = amountStr == null ? null : parseAmountMinor(amountStr); + if (amount == null || amount <= 0) continue; + + return ParseDraft( + rawMessageId: msg.id, + type: t.type, + amount: amount, + currency: 'RUB', + cardLast4: _group(t.extract['cardLast4'], m), + merchantRaw: _group(t.extract['merchant'], m)?.trim(), + counterpartyPhone: _group(t.extract['phone'], m), + kind: _kindFor(t.type), + source: ParseSource.regex, + ); + } + return null; + } + + /// Извлекает значение по back-reference вида `$1` из совпадения. + String? _group(String? ref, RegExpMatch m) { + if (ref == null) return null; + final n = int.tryParse(ref.replaceAll(r'$', '')); + if (n == null || n > m.groupCount) return null; + final v = m.group(n); + return (v == null || v.isEmpty) ? null : v; + } + + TxKind _kindFor(TransactionType type) => switch (type) { + TransactionType.expense => TxKind.purchase, + TransactionType.income => TxKind.transferIn, + TransactionType.transfer => TxKind.transferOut, + }; +} + +/// Парсит денежную строку РФ-формата в минорные единицы (копейки). +/// +/// Поддерживает пробелы/неразрывные пробелы как разделители тысяч и +/// `,`/`.` как десятичный разделитель: `«1 240,50»` → 124050, `«1240»` → 124000. +int? parseAmountMinor(String raw) { + final s = raw.replaceAll(RegExp(r'\s'), ''); + if (s.isEmpty) return null; + + final lastDot = s.lastIndexOf('.'); + final lastComma = s.lastIndexOf(','); + final decPos = lastDot > lastComma ? lastDot : lastComma; + + String intPart; + String fracPart; + if (decPos >= 0) { + final after = s.substring(decPos + 1); + // Десятичный разделитель — только если 1–2 цифры в хвосте. + if (after.isNotEmpty && after.length <= 2 && _digitsOnly(after)) { + intPart = s.substring(0, decPos); + fracPart = after.padRight(2, '0'); + } else { + intPart = s; + fracPart = '00'; + } + } else { + intPart = s; + fracPart = '00'; + } + + intPart = intPart.replaceAll(RegExp(r'[^\d]'), ''); + if (intPart.isEmpty) return null; + + final minor = int.tryParse(intPart + fracPart.substring(0, 2)); + return minor; +} + +bool _digitsOnly(String s) => RegExp(r'^\d+$').hasMatch(s); diff --git a/lib/src/features/notification_parsing/data/parser/rule_lookup.dart b/lib/src/features/notification_parsing/data/parser/rule_lookup.dart new file mode 100644 index 0000000..7735685 --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/rule_lookup.dart @@ -0,0 +1,81 @@ +import '../../domain/entities/parse_rule.dart'; +import '../../domain/enums.dart'; + +/// Проверяет, совпадает ли правило с сообщением. +/// +/// `contains`/`regex` матчатся по полному телу уведомления (как в редакторе +/// «Если SMS содержит …» и live-превью); `exact` — по точному совпадению +/// нормализованного имени мерчанта. +bool ruleMatches(ParseRule rule, {required String body, String? merchantRaw}) => + patternMatches(rule.pattern, rule.matchMode, + body: body, merchantRaw: merchantRaw); + +/// Низкоуровневый матч паттерна (для live-превью в редакторе, где правила +/// ещё нет). +bool patternMatches( + String rawPattern, + MatchMode mode, { + required String body, + String? merchantRaw, +}) { + final pattern = rawPattern.trim(); + if (pattern.isEmpty) return false; + + switch (mode) { + case MatchMode.contains: + return body.toLowerCase().contains(pattern.toLowerCase()); + case MatchMode.exact: + final target = (merchantRaw ?? body).trim().toLowerCase(); + return target == pattern.toLowerCase(); + case MatchMode.regex: + try { + return RegExp(pattern, caseSensitive: false).hasMatch(body); + } on FormatException { + return false; + } + } +} + +/// ГЛАВНЫЙ gate (§5, шаг 4): ищет активное правило `merchantToCategory`, +/// которое уже подтверждено пользователем для этого мерчанта. +/// +/// При конфликте (§9.4): более длинный pattern → больший matchCount → +/// больший priority. +ParseRule? findMerchantRule( + List rules, { + required String body, + String? merchantRaw, +}) { + final matches = rules + .where((r) => + r.enabled && + r.kind == ParseRuleKind.merchantToCategory && + ruleMatches(r, body: body, merchantRaw: merchantRaw)) + .toList() + ..sort(_bySpecificity); + return matches.isEmpty ? null : matches.first; +} + +/// Ищет активное правило-исключение (kind=ignore) для сообщения. +ParseRule? findIgnoreRule( + List rules, { + required String body, + String? merchantRaw, +}) { + final matches = rules + .where((r) => + r.enabled && + r.kind == ParseRuleKind.ignore && + ruleMatches(r, body: body, merchantRaw: merchantRaw)) + .toList() + ..sort(_bySpecificity); + return matches.isEmpty ? null : matches.first; +} + +int _bySpecificity(ParseRule a, ParseRule b) { + final byLen = b.pattern.length.compareTo(a.pattern.length); + if (byLen != 0) return byLen; + final byCount = b.matchCount.compareTo(a.matchCount); + if (byCount != 0) return byCount; + return b.priority.compareTo(a.priority); +} diff --git a/lib/src/features/notification_parsing/data/parser/rule_suggester.dart b/lib/src/features/notification_parsing/data/parser/rule_suggester.dart new file mode 100644 index 0000000..c1c64ab --- /dev/null +++ b/lib/src/features/notification_parsing/data/parser/rule_suggester.dart @@ -0,0 +1,23 @@ +import '../../domain/entities/rule_candidate.dart'; +import '../../domain/entities/rule_suggestion.dart'; +import 'merchant_normalizer.dart'; + +/// Формирует предложение правила для Inbox из наблюдённого кандидата (§9.1). +/// +/// Не персистируется — кешируется в `RawMessage.draftJson`. Подставляет +/// canonical-имя мерчанта и наиболее вероятную категорию в кнопку +/// «Создать правило «merchant → category»». +/// +/// [categoryCandidate] — кандидат kind=merchantToCategory с тем же rawValue +/// (его `resolvedValue` = categoryId). [categoryName] заполняется выше +/// (контроллером) по справочнику категорий. +RuleSuggestion suggestRule({ + required String merchantRaw, + RuleCandidate? categoryCandidate, +}) { + return RuleSuggestion( + merchantRaw: merchantRaw, + merchantCanonical: normalizeMerchant(merchantRaw), + categoryId: categoryCandidate?.resolvedValue, + ); +} diff --git a/lib/src/features/notification_parsing/data/repositories/account_bindings_repository_impl.dart b/lib/src/features/notification_parsing/data/repositories/account_bindings_repository_impl.dart new file mode 100644 index 0000000..63a2261 --- /dev/null +++ b/lib/src/features/notification_parsing/data/repositories/account_bindings_repository_impl.dart @@ -0,0 +1,87 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/account_binding.dart'; +import '../../domain/repositories/account_bindings_repository.dart'; +import '../drift/daos/account_bindings_dao.dart'; +import '../mappers/account_binding_mapper.dart'; + +class AccountBindingsRepositoryImpl implements AccountBindingsRepository { + const AccountBindingsRepositoryImpl(this._dao); + final AccountBindingsDao _dao; + + @override + Stream> watchByUser(String userId) => _dao + .watchByUser(userId) + .map((rows) => rows.map((r) => r.toDomain()).toList()); + + @override + Future findByPackageAndCard( + String userId, + String packageName, + String cardLast4, + ) async => + (await _dao.findByPackageAndCard(userId, packageName, cardLast4)) + ?.toDomain(); + + @override + Future findByBankKeyAndCard( + String userId, + String bankKey, + String cardLast4, + ) async => + (await _dao.findByBankKeyAndCard(userId, bankKey, cardLast4))?.toDomain(); + + @override + Future findByPhone(String userId, String phone) async => + (await _dao.findByPhone(userId, phone))?.toDomain(); + + @override + Future> findByPackageName( + String userId, + String packageName, + ) async => + (await _dao.findByPackageName(userId, packageName)) + .map((r) => r.toDomain()) + .toList(); + + @override + Future create({ + required String userId, + String? packageName, + String? bankKey, + String? cardLast4, + String? phone, + required String accountId, + }) async { + final id = const Uuid().v4(); + await _dao.insert( + AccountBindingsTableCompanion.insert( + id: id, + userId: userId, + packageName: Value(packageName), + bankKey: Value(bankKey), + cardLast4: Value(cardLast4), + phone: Value(phone), + accountId: accountId, + ), + ); + return AccountBinding( + id: id, + userId: userId, + packageName: packageName, + bankKey: bankKey, + cardLast4: cardLast4, + phone: phone, + accountId: accountId, + createdAt: DateTime.now(), + ); + } + + @override + Future incrementMatchCount(String id) => _dao.incrementMatchCount(id); + + @override + Future deleteById(String id) => _dao.deleteById(id); +} diff --git a/lib/src/features/notification_parsing/data/repositories/parse_rules_repository_impl.dart b/lib/src/features/notification_parsing/data/repositories/parse_rules_repository_impl.dart new file mode 100644 index 0000000..1ebf463 --- /dev/null +++ b/lib/src/features/notification_parsing/data/repositories/parse_rules_repository_impl.dart @@ -0,0 +1,89 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/parse_rule.dart'; +import '../../domain/enums.dart'; +import '../../domain/repositories/parse_rules_repository.dart'; +import '../drift/daos/parse_rules_dao.dart'; +import '../mappers/parse_rule_mapper.dart'; + +class ParseRulesRepositoryImpl implements ParseRulesRepository { + const ParseRulesRepositoryImpl(this._dao); + final ParseRulesDao _dao; + + @override + Stream> watchByUser(String userId) => _dao + .watchByUser(userId) + .map((rows) => rows.map((r) => r.toDomain()).toList()); + + @override + Future> getEnabledByUser(String userId) async => + (await _dao.getEnabledByUser(userId)).map((r) => r.toDomain()).toList(); + + @override + Future> getByUser(String userId) async => + (await _dao.getByUser(userId)).map((r) => r.toDomain()).toList(); + + @override + Future findById(String id) async => + (await _dao.findById(id))?.toDomain(); + + @override + Future create({ + required String userId, + required ParseRuleKind kind, + required MatchMode matchMode, + required String pattern, + int priority = 0, + String? merchantCanonical, + String? categoryId, + String? accountId, + }) async { + final id = const Uuid().v4(); + await _dao.insert( + ParseRulesTableCompanion.insert( + id: id, + userId: userId, + kind: kind, + pattern: pattern, + matchMode: Value(matchMode), + priority: Value(priority), + merchantCanonical: Value(merchantCanonical), + categoryId: Value(categoryId), + accountId: Value(accountId), + ), + ); + final row = await _dao.findById(id); + return row!.toDomain(); + } + + @override + Future update(ParseRule rule) => _dao.updateRow( + ParseRulesTableCompanion( + id: Value(rule.id), + kind: Value(rule.kind), + matchMode: Value(rule.matchMode), + pattern: Value(rule.pattern), + priority: Value(rule.priority), + merchantCanonical: Value(rule.merchantCanonical), + categoryId: Value(rule.categoryId), + accountId: Value(rule.accountId), + enabled: Value(rule.enabled), + ), + ); + + @override + Future setEnabled(String id, {required bool enabled}) => + _dao.setEnabled(id, enabled: enabled); + + @override + Future incrementMatchCount(String id, DateTime now) => + _dao.incrementMatchCount(id, now); + + @override + Future decrementWeight(String id) => _dao.decrementWeight(id); + + @override + Future deleteById(String id) => _dao.deleteById(id); +} diff --git a/lib/src/features/notification_parsing/data/repositories/raw_messages_repository_impl.dart b/lib/src/features/notification_parsing/data/repositories/raw_messages_repository_impl.dart new file mode 100644 index 0000000..fd3dd3b --- /dev/null +++ b/lib/src/features/notification_parsing/data/repositories/raw_messages_repository_impl.dart @@ -0,0 +1,105 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/raw_message.dart'; +import '../../domain/enums.dart'; +import '../../domain/repositories/raw_messages_repository.dart'; +import '../drift/daos/raw_messages_dao.dart'; +import '../mappers/raw_message_mapper.dart'; +import '../parser/dedup.dart'; + +class RawMessagesRepositoryImpl implements RawMessagesRepository { + const RawMessagesRepositoryImpl(this._dao); + final RawMessagesDao _dao; + + @override + Stream> watchPending(String userId) => _dao + .watchPending(userId) + .map((rows) => rows.map((r) => r.toDomain()).toList()); + + @override + Stream> watchInbox(String userId) => _dao + .watchInbox(userId) + .map((rows) => rows.map((r) => r.toDomain()).toList()); + + @override + Stream watchInboxCount(String userId) => _dao.watchInboxCount(userId); + + @override + Future findById(String id) async => + (await _dao.findById(id))?.toDomain(); + + @override + Future findByDedupHash(String userId, String dedupHash) async => + (await _dao.findByDedupHash(userId, dedupHash))?.toDomain(); + + @override + Future> recentByUser(String userId, DateTime since) async => + (await _dao.getRecentByUser(userId, since)) + .map((r) => r.toDomain()) + .toList(); + + @override + Future insertIncoming({ + required String userId, + required String packageName, + String? title, + required String body, + required DateTime receivedAt, + }) async { + final hash = computeDedupHash(packageName, body); + final existing = await _dao.findByDedupHash(userId, hash); + if (existing != null) return existing.toDomain(); + + final id = const Uuid().v4(); + await _dao.insert( + RawMessagesTableCompanion.insert( + id: id, + userId: userId, + packageName: packageName, + title: Value(title), + body: body, + receivedAt: receivedAt, + dedupHash: hash, + status: const Value(RawMessageStatus.pending), + ), + ); + final row = await _dao.findById(id); + return row!.toDomain(); + } + + @override + Future updateStatus(String id, RawMessageStatus status) => + _dao.updateStatus(id, status); + + @override + Future updateAfterParse({ + required String id, + required RawMessageStatus status, + String? draftJson, + int? confidenceAmount, + int? confidenceAccount, + int? confidenceType, + int? confidenceMerchant, + int? confidenceCategory, + }) => + _dao.updateAfterParse( + id: id, + status: status, + draftJson: draftJson, + confidenceAmount: confidenceAmount, + confidenceAccount: confidenceAccount, + confidenceType: confidenceType, + confidenceMerchant: confidenceMerchant, + confidenceCategory: confidenceCategory, + ); + + @override + Future linkTransaction(String id, String transactionId) => + _dao.linkTransaction(id, transactionId); + + @override + Future incrementParseAttempts(String id) => + _dao.incrementParseAttempts(id); +} diff --git a/lib/src/features/notification_parsing/data/repositories/rule_candidates_repository_impl.dart b/lib/src/features/notification_parsing/data/repositories/rule_candidates_repository_impl.dart new file mode 100644 index 0000000..c00e3ba --- /dev/null +++ b/lib/src/features/notification_parsing/data/repositories/rule_candidates_repository_impl.dart @@ -0,0 +1,60 @@ +import 'package:uuid/uuid.dart'; + +import '../../../../core/database/app_database.dart'; +import '../../domain/entities/rule_candidate.dart'; +import '../../domain/enums.dart'; +import '../../domain/repositories/rule_candidates_repository.dart'; +import '../drift/daos/rule_candidates_dao.dart'; +import '../mappers/rule_candidate_mapper.dart'; + +class RuleCandidatesRepositoryImpl implements RuleCandidatesRepository { + const RuleCandidatesRepositoryImpl(this._dao); + final RuleCandidatesDao _dao; + + @override + Stream> watchByUser(String userId) => _dao + .watchByUser(userId) + .map((rows) => rows.map((r) => r.toDomain()).toList()); + + @override + Future findByRawValue( + String userId, + ParseRuleKind kind, + String rawValue, + ) async => + (await _dao.findByRawValue(userId, kind, rawValue))?.toDomain(); + + @override + Future observe({ + required String userId, + required ParseRuleKind kind, + required String rawValue, + required String resolvedValue, + }) async { + final now = DateTime.now(); + final existing = await _dao.findByRawValue(userId, kind, rawValue); + if (existing != null) { + await _dao.incrementSeen( + existing.id, + resolvedValue: resolvedValue, + now: now, + ); + return; + } + await _dao.insert( + RuleCandidatesTableCompanion.insert( + id: const Uuid().v4(), + userId: userId, + kind: kind, + rawValue: rawValue, + resolvedValue: resolvedValue, + firstSeenAt: now, + lastSeenAt: now, + ), + ); + } + + @override + Future deleteByRawValue(String userId, String rawValue) => + _dao.deleteByRawValue(userId, rawValue); +} diff --git a/lib/src/features/notification_parsing/domain/enums.dart b/lib/src/features/notification_parsing/domain/enums.dart index 46ad88c..69b0235 100644 --- a/lib/src/features/notification_parsing/domain/enums.dart +++ b/lib/src/features/notification_parsing/domain/enums.dart @@ -1,5 +1,6 @@ /// Enums for the notification-parsing feature. /// Drift converters are defined in the data layer; these are pure-Dart. +library; // --------------------------------------------------------------------------- // RawMessageStatus diff --git a/lib/src/features/notification_parsing/domain/repositories/account_bindings_repository.dart b/lib/src/features/notification_parsing/domain/repositories/account_bindings_repository.dart new file mode 100644 index 0000000..91b3131 --- /dev/null +++ b/lib/src/features/notification_parsing/domain/repositories/account_bindings_repository.dart @@ -0,0 +1,40 @@ +import '../entities/account_binding.dart'; + +/// Доступ к привязкам «карта/телефон → счёт» ([AccountBinding]). +/// +/// Используется на шаге 3 pipeline (account_resolver) для разрешения accountId. +abstract interface class AccountBindingsRepository { + Stream> watchByUser(String userId); + + Future findByPackageAndCard( + String userId, + String packageName, + String cardLast4, + ); + + Future findByBankKeyAndCard( + String userId, + String bankKey, + String cardLast4, + ); + + Future findByPhone(String userId, String phone); + + Future> findByPackageName( + String userId, + String packageName, + ); + + Future create({ + required String userId, + String? packageName, + String? bankKey, + String? cardLast4, + String? phone, + required String accountId, + }); + + Future incrementMatchCount(String id); + + Future deleteById(String id); +} diff --git a/lib/src/features/notification_parsing/domain/repositories/parse_rules_repository.dart b/lib/src/features/notification_parsing/domain/repositories/parse_rules_repository.dart new file mode 100644 index 0000000..7d392bc --- /dev/null +++ b/lib/src/features/notification_parsing/domain/repositories/parse_rules_repository.dart @@ -0,0 +1,39 @@ +import '../entities/parse_rule.dart'; +import '../enums.dart'; + +/// Доступ к пользовательским правилам парсинга ([ParseRule]). +abstract interface class ParseRulesRepository { + /// Все правила пользователя — для экрана «Правила парсинга». + Stream> watchByUser(String userId); + + /// Только активные правила — для pipeline (rule_lookup). + Future> getEnabledByUser(String userId); + + Future> getByUser(String userId); + + Future findById(String id); + + /// Создаёт правило (активно сразу, weight=1). Возвращает сохранённую сущность. + Future create({ + required String userId, + required ParseRuleKind kind, + required MatchMode matchMode, + required String pattern, + int priority, + String? merchantCanonical, + String? categoryId, + String? accountId, + }); + + Future update(ParseRule rule); + + Future setEnabled(String id, {required bool enabled}); + + /// Инкремент matchCount + lastMatchAt при срабатывании. + Future incrementMatchCount(String id, DateTime now); + + /// Откат («правило сработало неправильно»): weight-- и деактивация при <=0. + Future decrementWeight(String id); + + Future deleteById(String id); +} diff --git a/lib/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart b/lib/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart new file mode 100644 index 0000000..6b9da3c --- /dev/null +++ b/lib/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart @@ -0,0 +1,54 @@ +import '../entities/raw_message.dart'; +import '../enums.dart'; + +/// Доступ к сырым входящим уведомлениям ([RawMessage]). +/// +/// Точка входа pipeline: сообщения создаются со статусом +/// [RawMessageStatus.pending], воркер их разбирает и переводит в +/// `applied` / `inbox` / `ignored` / `failed`. +abstract interface class RawMessagesRepository { + /// Необработанные сообщения — источник для ParsingWorker. + Stream> watchPending(String userId); + + /// Сообщения, ожидающие подтверждения в Inbox. + Stream> watchInbox(String userId); + + /// Реактивный счётчик Inbox — для бэджа на Home. + Stream watchInboxCount(String userId); + + Future findById(String id); + + Future findByDedupHash(String userId, String dedupHash); + + /// Сообщения за период (для live-превью правил). + Future> recentByUser(String userId, DateTime since); + + /// Идемпотентная вставка нового уведомления. Если сообщение с тем же + /// [dedupHash] уже есть — возвращает существующее, не создавая дубль. + Future insertIncoming({ + required String userId, + required String packageName, + String? title, + required String body, + required DateTime receivedAt, + }); + + Future updateStatus(String id, RawMessageStatus status); + + /// Запись результатов парсинга (draft + per-field confidence). + Future updateAfterParse({ + required String id, + required RawMessageStatus status, + String? draftJson, + int? confidenceAmount, + int? confidenceAccount, + int? confidenceType, + int? confidenceMerchant, + int? confidenceCategory, + }); + + /// Привязка к созданной транзакции (status → applied). + Future linkTransaction(String id, String transactionId); + + Future incrementParseAttempts(String id); +} diff --git a/lib/src/features/notification_parsing/domain/repositories/rule_candidates_repository.dart b/lib/src/features/notification_parsing/domain/repositories/rule_candidates_repository.dart new file mode 100644 index 0000000..5b898cc --- /dev/null +++ b/lib/src/features/notification_parsing/domain/repositories/rule_candidates_repository.dart @@ -0,0 +1,26 @@ +import '../entities/rule_candidate.dart'; +import '../enums.dart'; + +/// Доступ к накопленным наблюдениям ([RuleCandidate]) — источнику +/// предложений правил для Inbox. +abstract interface class RuleCandidatesRepository { + Stream> watchByUser(String userId); + + Future findByRawValue( + String userId, + ParseRuleKind kind, + String rawValue, + ); + + /// Наблюдение: создаёт кандидата или инкрементит seenCount существующего, + /// обновляя resolvedValue и lastSeenAt. + Future observe({ + required String userId, + required ParseRuleKind kind, + required String rawValue, + required String resolvedValue, + }); + + /// Удаляет всех кандидатов для rawValue (при создании правила). + Future deleteByRawValue(String userId, String rawValue); +} diff --git a/lib/src/features/notification_parsing/presentation/screens/debug_inject_screen.dart b/lib/src/features/notification_parsing/presentation/screens/debug_inject_screen.dart new file mode 100644 index 0000000..106fcc4 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/screens/debug_inject_screen.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../user/application/active_user_controller.dart'; +import '../../application/notification_parsing_providers.dart'; + +/// Debug-инжектор сырых уведомлений (только в debug-сборке). +/// +/// Заменяет нативный `NotificationListenerService` (Phase 0) для прогона +/// pipeline на эмуляторе: создаёт `raw_messages` со статусом pending, дальше +/// их подхватывает [ParsingWorker]. +class DebugInjectScreen extends ConsumerStatefulWidget { + const DebugInjectScreen({super.key}); + + @override + ConsumerState createState() => _DebugInjectScreenState(); +} + +class _DebugInjectScreenState extends ConsumerState { + final _packageCtrl = TextEditingController(text: 'ru.sberbankmobile'); + final _titleCtrl = TextEditingController(); + final _bodyCtrl = TextEditingController(); + + @override + void dispose() { + _packageCtrl.dispose(); + _titleCtrl.dispose(); + _bodyCtrl.dispose(); + super.dispose(); + } + + void _fillSample() { + _packageCtrl.text = 'ru.sberbankmobile'; + _bodyCtrl.text = + 'Покупка 1240 ₽ Карта *3456 PYATEROCHKA 14:05 Баланс: 5000 ₽'; + setState(() {}); + } + + Future _send() async { + final userId = ref.read(activeUserControllerProvider).value?.id; + final body = _bodyCtrl.text.trim(); + if (userId == null || body.isEmpty) return; + + await ref.read(rawMessagesRepositoryProvider).insertIncoming( + userId: userId, + packageName: _packageCtrl.text.trim(), + title: _titleCtrl.text.trim().isEmpty ? null : _titleCtrl.text.trim(), + body: body, + receivedAt: DateTime.now(), + ); + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.debugInjectSent)), + ); + _bodyCtrl.clear(); + _titleCtrl.clear(); + } + + @override + Widget build(BuildContext context) { + final p = context.palette; + final l10n = context.l10n; + + return Scaffold( + backgroundColor: p.paper, + appBar: AppBar( + backgroundColor: p.paper, + title: Text(l10n.debugInjectTitle), + actions: [ + TextButton( + onPressed: _fillSample, + child: Text(l10n.debugInjectFillSample), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _Field(label: l10n.debugInjectPackageLabel, controller: _packageCtrl), + const SizedBox(height: 12), + _Field(label: l10n.debugInjectTitleLabel, controller: _titleCtrl), + const SizedBox(height: 12), + _Field( + label: l10n.debugInjectBodyLabel, + controller: _bodyCtrl, + maxLines: 4, + ), + const SizedBox(height: 20), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: p.accent, + minimumSize: const Size.fromHeight(48), + ), + onPressed: _send, + child: Text(l10n.debugInjectSend), + ), + ], + ), + ); + } +} + +class _Field extends StatelessWidget { + const _Field({ + required this.label, + required this.controller, + this.maxLines = 1, + }); + + final String label; + final TextEditingController controller; + final int maxLines; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 6), + TextField( + controller: controller, + maxLines: maxLines, + decoration: InputDecoration( + filled: true, + fillColor: p.paper2, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: p.line), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/screens/inbox_screen.dart b/lib/src/features/notification_parsing/presentation/screens/inbox_screen.dart new file mode 100644 index 0000000..79401c3 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/screens/inbox_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../accounts/application/accounts_controller.dart'; +import '../../../categories/application/categories_controller.dart'; +import '../../../categories/domain/entities/category.dart'; +import '../../../user/application/active_user_controller.dart'; +import '../../application/inbox_controller.dart'; +import '../../domain/entities/raw_message.dart'; +import '../widgets/inbox_card.dart'; + +/// Inbox: сообщения, ожидающие подтверждения (§12.2). +class InboxScreen extends ConsumerWidget { + const InboxScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final l10n = context.l10n; + final userId = ref.watch(activeUserControllerProvider).value?.id; + + if (userId == null) { + return Scaffold(backgroundColor: p.paper, body: const SizedBox.shrink()); + } + + final messages = + ref.watch(inboxMessagesProvider(userId)).value ?? const []; + final categories = + ref.watch(categoriesStreamProvider(userId)).value ?? const []; + final categoryById = {for (final c in categories) c.id: c}; + final defaultAccountId = ref.watch(defaultAccountProvider(userId)).value?.id; + + return Scaffold( + backgroundColor: p.paper, + appBar: AppBar( + backgroundColor: p.paper, + title: Text( + messages.isEmpty + ? l10n.inboxTitle + : '${l10n.inboxTitle} (${messages.length})', + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), + child: Text( + l10n.inboxSubtitle, + style: TextStyle(fontSize: 12, color: p.ink2, height: 1.3), + ), + ), + Expanded( + child: messages.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + l10n.inboxEmpty, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: p.ink2), + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.only(bottom: 12), + itemCount: messages.length, + itemBuilder: (context, i) => InboxCard( + message: messages[i], + userId: userId, + defaultAccountId: defaultAccountId, + categoryById: categoryById, + ), + ), + ), + if (messages.isNotEmpty) + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 8), + child: Row( + children: [ + Expanded( + child: FilledButton( + style: FilledButton.styleFrom(backgroundColor: p.accent), + onPressed: () => ref + .read(inboxControllerProvider.notifier) + .applyAll(userId), + child: Text(l10n.inboxApplyAll), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: p.ink, + side: BorderSide(color: p.line), + ), + onPressed: () => ref + .read(inboxControllerProvider.notifier) + .hideAll(userId), + child: Text(l10n.inboxHideAll), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/screens/parsing_settings_screen.dart b/lib/src/features/notification_parsing/presentation/screens/parsing_settings_screen.dart new file mode 100644 index 0000000..c1fc4d5 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/screens/parsing_settings_screen.dart @@ -0,0 +1,185 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../user/application/active_user_controller.dart'; +import '../../application/parsing_settings_controller.dart'; +import '../../application/rules_controller.dart'; +import '../../domain/entities/parse_rule.dart'; + +/// Экран настроек парсинга (§12.5). Phase 1: тумблер фичи + строгость +/// авто-добавления + вход в правила. AI-секция — заглушка под Phase 2. +class ParsingSettingsScreen extends ConsumerWidget { + const ParsingSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final l10n = context.l10n; + final userId = ref.watch(activeUserControllerProvider).value?.id; + final settings = ref.watch(parsingSettingsControllerProvider).value; + final controller = ref.read(parsingSettingsControllerProvider.notifier); + + final rulesCount = userId == null + ? 0 + : (ref.watch(parseRulesListProvider(userId)).value ?? + const []) + .length; + + return Scaffold( + backgroundColor: p.paper, + appBar: AppBar( + backgroundColor: p.paper, + title: Text(l10n.parsingSettingsTitle), + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + children: [ + _Card( + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 14), + title: Text(l10n.parsingEnableLabel, + style: TextStyle(fontSize: 14, color: p.ink)), + value: settings?.enabled ?? true, + activeThumbColor: p.accent, + onChanged: settings == null + ? null + : (v) => controller.setEnabled(v), + ), + ], + ), + const SizedBox(height: 16), + Text(l10n.parsingStrictnessLabel, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 8), + _StrictnessSelector( + value: settings?.strictness ?? 85, + onChanged: (v) => controller.setStrictness(v), + ), + const SizedBox(height: 6), + Text(l10n.parsingStrictnessHint, + style: TextStyle(fontSize: 12, color: p.ink2)), + const SizedBox(height: 16), + _Card( + children: [ + _NavTile( + icon: Icons.rule_outlined, + title: l10n.parsingRulesTile, + onTap: () => context.push(AppRoutes.parsingRules), + ), + ], + ), + const SizedBox(height: 16), + Text(l10n.parsingAiSectionTitle, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 8), + _Card( + children: [ + ListTile( + leading: Icon(Icons.smart_toy_outlined, color: p.line2), + title: Text(l10n.parsingAiComingSoon, + style: TextStyle(fontSize: 13, color: p.ink2)), + enabled: false, + ), + ], + ), + const SizedBox(height: 16), + Text(l10n.parsingRulesCreatedCount(rulesCount), + style: TextStyle(fontSize: 12, color: p.ink2)), + if (kDebugMode) ...[ + const SizedBox(height: 16), + Text(l10n.parsingDebugSectionTitle, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 8), + _Card( + children: [ + _NavTile( + icon: Icons.bug_report_outlined, + title: l10n.parsingDebugInjectTile, + onTap: () => context.push(AppRoutes.parsingDebugInject), + ), + ], + ), + ], + ], + ), + ); + } +} + +class _StrictnessSelector extends StatelessWidget { + const _StrictnessSelector({required this.value, required this.onChanged}); + + final int value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return SegmentedButton( + segments: [ + ButtonSegment(value: 75, label: Text(l10n.parsingStrictnessSoft)), + ButtonSegment(value: 85, label: Text(l10n.parsingStrictnessNormal)), + ButtonSegment(value: 95, label: Text(l10n.parsingStrictnessStrict)), + ], + selected: {value}, + showSelectedIcon: false, + onSelectionChanged: (s) => onChanged(s.first), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.children}); + final List children; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return Container( + decoration: BoxDecoration( + border: Border.all(color: p.line), + borderRadius: BorderRadius.circular(14), + ), + child: Column(children: children), + ); + } +} + +class _NavTile extends StatelessWidget { + const _NavTile({ + required this.icon, + required this.title, + required this.onTap, + }); + + final IconData icon; + final String title; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + child: Row( + children: [ + Icon(icon, size: 20, color: p.ink2), + const SizedBox(width: 12), + Expanded( + child: Text(title, style: TextStyle(fontSize: 14, color: p.ink)), + ), + Icon(Icons.chevron_right, size: 18, color: p.ink2), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/screens/rule_editor_screen.dart b/lib/src/features/notification_parsing/presentation/screens/rule_editor_screen.dart new file mode 100644 index 0000000..82cad8d --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/screens/rule_editor_screen.dart @@ -0,0 +1,497 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../core/database/converters/enum_converters.dart'; +import '../../../accounts/application/accounts_controller.dart'; +import '../../../accounts/domain/entities/account.dart'; +import '../../../categories/application/categories_controller.dart'; +import '../../../categories/domain/entities/category.dart'; +import '../../../transactions/presentation/widgets/account_picker_sheet.dart'; +import '../../../transactions/presentation/widgets/category_picker_sheet.dart'; +import '../../../user/application/active_user_controller.dart'; +import '../../application/rules_controller.dart'; +import '../../domain/entities/parse_rule.dart'; +import '../../domain/enums.dart'; + +/// Предзаполнение редактора при создании правила из Inbox. +class RuleEditorPrefill { + const RuleEditorPrefill({ + required this.pattern, + required this.merchantCanonical, + this.categoryId, + this.accountId, + this.matchMode = MatchMode.contains, + }); + + final String pattern; + final String merchantCanonical; + final String? categoryId; + final String? accountId; + final MatchMode matchMode; +} + +/// Результат редактора в режиме compose (возвращается через pop в Inbox). +class RuleEditorResult { + const RuleEditorResult({ + required this.matchMode, + required this.pattern, + required this.merchantCanonical, + this.categoryId, + this.accountId, + this.priority = 0, + }); + + final MatchMode matchMode; + final String pattern; + final String merchantCanonical; + final String? categoryId; + final String? accountId; + final int priority; +} + +/// Редактор правила (§9.3). Два режима: +/// - [ruleId] != null — правка существующего правила (пишет в БД, есть «Удалить»); +/// - иначе — compose из Inbox: возвращает [RuleEditorResult] через `pop`. +class RuleEditorScreen extends ConsumerStatefulWidget { + const RuleEditorScreen({super.key, this.ruleId, this.prefill}); + + final String? ruleId; + final RuleEditorPrefill? prefill; + + @override + ConsumerState createState() => _RuleEditorScreenState(); +} + +class _RuleEditorScreenState extends ConsumerState { + final _patternCtrl = TextEditingController(); + final _merchantCtrl = TextEditingController(); + MatchMode _matchMode = MatchMode.contains; + String? _categoryId; + String? _accountId; + int _priority = 0; + bool _enabled = true; + bool _advanced = false; + bool _initialized = false; + + bool get _isEdit => widget.ruleId != null; + + @override + void initState() { + super.initState(); + final pf = widget.prefill; + if (pf != null) { + _patternCtrl.text = pf.pattern; + _merchantCtrl.text = pf.merchantCanonical; + _matchMode = pf.matchMode; + _categoryId = pf.categoryId; + _accountId = pf.accountId; + _initialized = true; + } + } + + @override + void dispose() { + _patternCtrl.dispose(); + _merchantCtrl.dispose(); + super.dispose(); + } + + void _hydrateFromRule(ParseRule rule) { + if (_initialized) return; + _patternCtrl.text = rule.pattern; + _merchantCtrl.text = rule.merchantCanonical ?? ''; + _matchMode = rule.matchMode; + _categoryId = rule.categoryId; + _accountId = rule.accountId; + _priority = rule.priority; + _enabled = rule.enabled; + _initialized = true; + } + + Future _save(String userId, ParseRule? existing) async { + final pattern = _patternCtrl.text.trim(); + if (pattern.isEmpty) return; + final merchant = _merchantCtrl.text.trim(); + + if (_isEdit && existing != null) { + await ref.read(rulesControllerProvider.notifier).update( + existing.copyWith( + matchMode: _matchMode, + pattern: pattern, + merchantCanonical: merchant.isEmpty ? null : merchant, + categoryId: _categoryId, + accountId: _accountId, + priority: _priority, + enabled: _enabled, + ), + ); + if (mounted) context.pop(); + } else { + context.pop( + RuleEditorResult( + matchMode: _matchMode, + pattern: pattern, + merchantCanonical: merchant, + categoryId: _categoryId, + accountId: _accountId, + priority: _priority, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final p = context.palette; + final l10n = context.l10n; + final userId = ref.watch(activeUserControllerProvider).value?.id; + + if (userId == null) { + return const Scaffold(body: SizedBox.shrink()); + } + + ParseRule? existing; + if (_isEdit) { + existing = ref.watch(parseRuleByIdProvider(widget.ruleId!)).value; + if (existing != null) _hydrateFromRule(existing); + } + + final categories = + ref.watch(categoriesStreamProvider(userId)).value ?? const []; + final categoryName = _categoryId == null + ? null + : categories.where((c) => c.id == _categoryId).firstOrNull?.name; + final accounts = + ref.watch(accountsStreamProvider(userId)).value ?? const []; + final accountName = _accountId == null + ? null + : accounts.where((a) => a.id == _accountId).firstOrNull?.name; + + return Scaffold( + backgroundColor: p.paper, + appBar: AppBar( + backgroundColor: p.paper, + title: Text(_isEdit ? l10n.ruleEditorEditTitle : l10n.ruleEditorNewTitle), + actions: [ + if (_isEdit) + IconButton( + icon: Icon(Icons.delete_outline, color: p.negative), + onPressed: () async { + await ref + .read(rulesControllerProvider.notifier) + .delete(widget.ruleId!); + if (context.mounted) context.pop(); + }, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + children: [ + Text(l10n.ruleEditorIfContains, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 6), + TextField( + controller: _patternCtrl, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + hintText: l10n.ruleEditorPatternHint, + filled: true, + fillColor: p.paper2, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: p.line), + ), + ), + ), + const SizedBox(height: 10), + _MatchModeSelector( + mode: _matchMode, + onChanged: (m) => setState(() => _matchMode = m), + ), + const SizedBox(height: 18), + Text(l10n.ruleEditorThen, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 8), + _FieldRow( + label: l10n.ruleEditorMerchant, + child: TextField( + controller: _merchantCtrl, + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: l10n.ruleEditorMerchantHint, + isDense: true, + border: InputBorder.none, + ), + ), + ), + _Divider(color: p.line), + _PickerRow( + label: l10n.ruleEditorCategory, + value: categoryName ?? l10n.inboxNoCategory, + onTap: () async { + final id = await showCategoryPicker( + context, + userId: userId, + type: CategoryType.expense, + currentCategoryId: _categoryId, + ); + if (id != null) setState(() => _categoryId = id); + }, + ), + _Divider(color: p.line), + _PickerRow( + label: l10n.ruleEditorAccount, + value: accountName ?? l10n.ruleEditorAccountUnchanged, + onTap: () async { + final id = await showAccountPicker( + context, + userId: userId, + currentAccountId: _accountId, + ); + if (id != null) setState(() => _accountId = id); + }, + ), + const SizedBox(height: 12), + _AdvancedToggle( + expanded: _advanced, + onTap: () => setState(() => _advanced = !_advanced), + label: l10n.ruleEditorAdvanced, + ), + if (_advanced) ...[ + const SizedBox(height: 8), + _FieldRow( + label: l10n.ruleEditorPriority, + child: DropdownButton( + value: _priority, + underline: const SizedBox.shrink(), + items: const [ + DropdownMenuItem(value: 0, child: Text('0')), + DropdownMenuItem(value: 1, child: Text('+1')), + DropdownMenuItem(value: 2, child: Text('+2')), + ], + onChanged: (v) => setState(() => _priority = v ?? 0), + ), + ), + if (_isEdit) + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(l10n.ruleEditorEnabledLabel, + style: TextStyle(fontSize: 14, color: p.ink)), + value: _enabled, + activeThumbColor: p.accent, + onChanged: (v) => setState(() => _enabled = v), + ), + ], + const SizedBox(height: 18), + _MatchesPreview( + userId: userId, + pattern: _patternCtrl.text, + mode: _matchMode, + ), + const SizedBox(height: 18), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: p.accent, + minimumSize: const Size.fromHeight(48), + ), + onPressed: _patternCtrl.text.trim().isEmpty + ? null + : () => _save(userId, existing), + child: Text(l10n.ruleEditorSave), + ), + ], + ), + ); + } +} + +class _MatchesPreview extends ConsumerWidget { + const _MatchesPreview({ + required this.userId, + required this.pattern, + required this.mode, + }); + + final String userId; + final String pattern; + final MatchMode mode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final l10n = context.l10n; + if (pattern.trim().isEmpty) return const SizedBox.shrink(); + final matches = + ref.watch(ruleMatchPreviewProvider(userId, pattern, mode)).value ?? + const []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.ruleEditorMatchesTitle, + style: TextStyle(fontSize: 13, color: p.ink2)), + const SizedBox(height: 8), + if (matches.isEmpty) + Text(l10n.ruleEditorNoMatches, + style: TextStyle(fontSize: 13, color: p.ink2)) + else + ...matches.take(30).map( + (m) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Icon(Icons.check, size: 14, color: p.positive), + const SizedBox(width: 8), + Expanded( + child: Text( + m.body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12, color: p.ink2), + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _MatchModeSelector extends StatelessWidget { + const _MatchModeSelector({required this.mode, required this.onChanged}); + + final MatchMode mode; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final p = context.palette; + final items = <(MatchMode, String)>[ + (MatchMode.contains, l10n.ruleEditorModeContains), + (MatchMode.exact, l10n.ruleEditorModeExact), + (MatchMode.regex, l10n.ruleEditorModeRegex), + ]; + return Row( + children: [ + for (final (m, label) in items) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(label), + selected: mode == m, + selectedColor: p.accentSoft, + onSelected: (_) => onChanged(m), + ), + ), + ], + ); + } +} + +class _FieldRow extends StatelessWidget { + const _FieldRow({required this.label, required this.child}); + + final String label; + final Widget child; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + SizedBox( + width: 90, + child: Text(label, style: TextStyle(fontSize: 14, color: p.ink2)), + ), + Expanded(child: child), + ], + ), + ); + } +} + +class _PickerRow extends StatelessWidget { + const _PickerRow({ + required this.label, + required this.value, + required this.onTap, + }); + + final String label; + final String value; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + SizedBox( + width: 90, + child: Text(label, style: TextStyle(fontSize: 14, color: p.ink2)), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: TextStyle(fontSize: 14, color: p.ink), + ), + ), + Icon(Icons.chevron_right, size: 18, color: p.ink2), + ], + ), + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider({required this.color}); + final Color color; + @override + Widget build(BuildContext context) => Container(height: 1, color: color); +} + +class _AdvancedToggle extends StatelessWidget { + const _AdvancedToggle({ + required this.expanded, + required this.onTap, + required this.label, + }); + + final bool expanded; + final VoidCallback onTap; + final String label; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon(expanded ? Icons.expand_less : Icons.expand_more, + size: 18, color: p.ink2), + const SizedBox(width: 6), + Text(label, style: TextStyle(fontSize: 14, color: p.ink2)), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/screens/rules_list_screen.dart b/lib/src/features/notification_parsing/presentation/screens/rules_list_screen.dart new file mode 100644 index 0000000..a8e7030 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/screens/rules_list_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../categories/application/categories_controller.dart'; +import '../../../categories/domain/entities/category.dart'; +import '../../../user/application/active_user_controller.dart'; +import '../../application/rules_controller.dart'; +import '../../domain/entities/parse_rule.dart'; +import '../../domain/enums.dart'; +import '../widgets/rule_card.dart'; +import 'rule_editor_screen.dart'; + +enum _RuleFilter { all, merchants, accounts, ignore } + +/// Список правил парсинга (§12.3). +class RulesListScreen extends ConsumerStatefulWidget { + const RulesListScreen({super.key}); + + @override + ConsumerState createState() => _RulesListScreenState(); +} + +class _RulesListScreenState extends ConsumerState { + _RuleFilter _filter = _RuleFilter.all; + + bool _matches(ParseRule r) => switch (_filter) { + _RuleFilter.all => true, + _RuleFilter.merchants => r.kind == ParseRuleKind.merchantToCategory, + _RuleFilter.accounts => r.kind == ParseRuleKind.senderToAccount, + _RuleFilter.ignore => r.kind == ParseRuleKind.ignore, + }; + + @override + Widget build(BuildContext context) { + final p = context.palette; + final l10n = context.l10n; + final userId = ref.watch(activeUserControllerProvider).value?.id; + + if (userId == null) { + return Scaffold(backgroundColor: p.paper, body: const SizedBox.shrink()); + } + + final rules = + (ref.watch(parseRulesListProvider(userId)).value ?? const []) + .where(_matches) + .toList(); + final categories = + ref.watch(categoriesStreamProvider(userId)).value ?? const []; + final categoryById = {for (final c in categories) c.id: c}; + + return Scaffold( + backgroundColor: p.paper, + appBar: AppBar( + backgroundColor: p.paper, + title: Text(l10n.rulesTitle), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => _createRule(context, userId), + ), + ], + ), + body: Column( + children: [ + SizedBox( + height: 48, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12), + children: [ + for (final entry in <(_RuleFilter, String)>[ + (_RuleFilter.all, l10n.rulesFilterAll), + (_RuleFilter.merchants, l10n.rulesFilterMerchants), + (_RuleFilter.accounts, l10n.rulesFilterAccounts), + (_RuleFilter.ignore, l10n.rulesFilterIgnore), + ]) + Padding( + padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8), + child: ChoiceChip( + label: Text(entry.$2), + selected: _filter == entry.$1, + selectedColor: p.accentSoft, + onSelected: (_) => setState(() => _filter = entry.$1), + ), + ), + ], + ), + ), + Expanded( + child: rules.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text(l10n.rulesEmpty, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: p.ink2)), + ), + ) + : ListView.separated( + itemCount: rules.length, + separatorBuilder: (_, _) => + Container(height: 1, color: p.line), + itemBuilder: (context, i) => RuleCard( + rule: rules[i], + categoryById: categoryById, + onTap: () => + context.push(AppRoutes.parsingRuleEdit(rules[i].id)), + onToggle: (enabled) => ref + .read(rulesControllerProvider.notifier) + .setEnabled(rules[i].id, enabled: enabled), + ), + ), + ), + ], + ), + ); + } + + Future _createRule(BuildContext context, String userId) async { + final result = + await context.push(AppRoutes.parsingRuleNew); + if (result == null) return; + await ref.read(rulesControllerProvider.notifier).create( + userId: userId, + kind: ParseRuleKind.merchantToCategory, + matchMode: result.matchMode, + pattern: result.pattern, + priority: result.priority, + merchantCanonical: + result.merchantCanonical.isEmpty ? null : result.merchantCanonical, + categoryId: result.categoryId, + accountId: result.accountId, + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/widgets/confidence_badge.dart b/lib/src/features/notification_parsing/presentation/widgets/confidence_badge.dart new file mode 100644 index 0000000..7c20fa5 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/widgets/confidence_badge.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import '../../../../app/theme/app_colors.dart'; + +/// Поле считается слабым, если оценка известна и ниже порога. +/// (Проценты пользователю не показываем — только подсветку «?».) +bool isWeakScore(int? score) => score != null && score < 60; + +/// Маленький бэдж «?» рядом со слабым полем в Inbox (§8.2, §12.2). +class ConfidenceBadge extends StatelessWidget { + const ConfidenceBadge({super.key, this.size = 16}); + + final double size; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: p.negative.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + child: Text( + '?', + style: TextStyle( + fontSize: size * 0.7, + height: 1, + fontWeight: FontWeight.w700, + color: p.negative, + ), + ), + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart b/lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart new file mode 100644 index 0000000..9a623e2 --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart @@ -0,0 +1,389 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/router/app_routes.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../core/database/converters/enum_converters.dart'; +import '../../../categories/domain/entities/category.dart'; +import '../../../home/presentation/widgets/money_text.dart'; +import '../../application/inbox_controller.dart'; +import '../../data/parser/draft_codec.dart'; +import '../../domain/entities/raw_message.dart'; +import '../screens/rule_editor_screen.dart'; +import 'confidence_badge.dart'; + +/// Карточка Inbox (§12.2): мерчант, сумма, подсветка слабых полей и три +/// действия — «Создать правило», «Подтвердить разово», «Игнорировать». +class InboxCard extends ConsumerWidget { + const InboxCard({ + super.key, + required this.message, + required this.userId, + required this.defaultAccountId, + required this.categoryById, + }); + + final RawMessage message; + final String userId; + final String? defaultAccountId; + final Map categoryById; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final bundle = decodeDraftBundle(message.draftJson); + + return Container( + margin: const EdgeInsets.fromLTRB(12, 6, 12, 6), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: p.paper2, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: p.line), + ), + child: bundle == null + ? _UnrecognizedBody(message: message) + : _RecognizedBody( + message: message, + userId: userId, + defaultAccountId: defaultAccountId, + categoryById: categoryById, + bundle: bundle, + ), + ); + } +} + +class _RecognizedBody extends ConsumerWidget { + const _RecognizedBody({ + required this.message, + required this.userId, + required this.defaultAccountId, + required this.categoryById, + required this.bundle, + }); + + final RawMessage message; + final String userId; + final String? defaultAccountId; + final Map categoryById; + final DraftBundle bundle; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final l10n = context.l10n; + final draft = bundle.draft; + final sugg = bundle.suggestion; + + final merchant = sugg?.merchantCanonical ?? + draft.merchantCanonical ?? + draft.merchantRaw ?? + '—'; + final categoryId = draft.categoryId ?? sugg?.categoryId; + final categoryName = + categoryId != null ? categoryById[categoryId]?.name : null; + final accountId = draft.accountId ?? defaultAccountId; + + final signed = + draft.type == TransactionType.expense ? -draft.amount : draft.amount; + final amountColor = + draft.type == TransactionType.expense ? p.negative : p.positive; + + final weakOther = isWeakScore(message.confidenceAmount) || + isWeakScore(message.confidenceAccount) || + isWeakScore(message.confidenceType); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + merchant, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: p.ink, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + MoneyText(signed, + color: amountColor, fontSize: 15, fontWeight: FontWeight.w700, + withSign: true), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + if (weakOther) ...[ + const ConfidenceBadge(), + const SizedBox(width: 6), + ], + Flexible( + child: Text( + [ + categoryName ?? l10n.inboxNoCategory, + if (draft.cardLast4 != null) '· *${draft.cardLast4}', + ].join(' '), + style: TextStyle(fontSize: 13, color: p.ink2), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + message.body, + style: TextStyle(fontSize: 12, color: p.ink2, height: 1.3), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + _CreateRuleButton( + label: categoryName != null + ? l10n.inboxCreateRule(merchant, categoryName) + : l10n.inboxCreateRuleNoCategory(merchant), + onTap: () => _createRule( + context, + ref, + accountId: accountId, + categoryId: categoryId, + merchant: merchant, + ), + onEdit: () => _openEditor( + context, + ref, + accountId: accountId, + categoryId: categoryId, + merchant: merchant, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _SecondaryButton( + icon: Icons.check, + label: l10n.inboxConfirmOnce, + onTap: accountId == null + ? null + : () => ref.read(inboxControllerProvider.notifier).confirmOnce( + userId: userId, + message: message, + draft: draft, + accountId: accountId, + categoryId: categoryId, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _SecondaryButton( + icon: Icons.close, + label: l10n.inboxIgnore, + onTap: () => + ref.read(inboxControllerProvider.notifier).ignore(message), + ), + ), + ], + ), + ], + ); + } + + Future _createRule( + BuildContext context, + WidgetRef ref, { + required String? accountId, + required String? categoryId, + required String merchant, + }) async { + if (accountId == null) return; + final draft = bundle.draft; + if (categoryId == null) { + await _openEditor(context, ref, + accountId: accountId, categoryId: categoryId, merchant: merchant); + return; + } + await ref.read(inboxControllerProvider.notifier).createRule( + userId: userId, + message: message, + draft: draft, + accountId: accountId, + categoryId: categoryId, + merchantCanonical: merchant, + pattern: draft.merchantRaw ?? merchant, + ); + } + + Future _openEditor( + BuildContext context, + WidgetRef ref, { + required String? accountId, + required String? categoryId, + required String merchant, + }) async { + final draft = bundle.draft; + final result = await context.push( + AppRoutes.parsingRuleNew, + extra: RuleEditorPrefill( + pattern: draft.merchantRaw ?? merchant, + merchantCanonical: merchant, + categoryId: categoryId, + accountId: accountId, + ), + ); + if (result == null) return; + final acc = result.accountId ?? accountId; + if (acc == null) return; + await ref.read(inboxControllerProvider.notifier).createRule( + userId: userId, + message: message, + draft: draft, + accountId: acc, + categoryId: result.categoryId, + merchantCanonical: + result.merchantCanonical.isEmpty ? merchant : result.merchantCanonical, + pattern: result.pattern, + matchMode: result.matchMode, + ); + } +} + +class _UnrecognizedBody extends ConsumerWidget { + const _UnrecognizedBody({required this.message}); + + final RawMessage message; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final p = context.palette; + final l10n = context.l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(l10n.inboxUnrecognized, + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w600, color: p.ink)), + const SizedBox(height: 6), + Text(message.body, + style: TextStyle(fontSize: 12, color: p.ink2, height: 1.3), + maxLines: 3, + overflow: TextOverflow.ellipsis), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _SecondaryButton( + icon: Icons.edit_outlined, + label: l10n.inboxAddManually, + onTap: () => context.push(AppRoutes.transactionNew), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _SecondaryButton( + icon: Icons.close, + label: l10n.inboxIgnore, + onTap: () => + ref.read(inboxControllerProvider.notifier).ignore(message), + ), + ), + ], + ), + ], + ); + } +} + +class _CreateRuleButton extends StatelessWidget { + const _CreateRuleButton({ + required this.label, + required this.onTap, + required this.onEdit, + }); + + final String label; + final VoidCallback onTap; + final VoidCallback onEdit; + + @override + Widget build(BuildContext context) { + final p = context.palette; + final l10n = context.l10n; + return Material( + color: p.accent, + borderRadius: BorderRadius.circular(12), + child: Row( + children: [ + Expanded( + child: InkWell( + borderRadius: const BorderRadius.horizontal(left: Radius.circular(12)), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + Container(width: 1, height: 28, color: Colors.white24), + InkWell( + borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)), + onTap: onEdit, + child: Tooltip( + message: l10n.inboxEditRuleTooltip, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Icon(Icons.edit_outlined, size: 18, color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} + +class _SecondaryButton extends StatelessWidget { + const _SecondaryButton({ + required this.icon, + required this.label, + required this.onTap, + }); + + final IconData icon; + final String label; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final p = context.palette; + return OutlinedButton.icon( + onPressed: onTap, + style: OutlinedButton.styleFrom( + foregroundColor: p.ink, + side: BorderSide(color: p.line), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + icon: Icon(icon, size: 16), + label: Text(label, + style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis), + ); + } +} diff --git a/lib/src/features/notification_parsing/presentation/widgets/rule_card.dart b/lib/src/features/notification_parsing/presentation/widgets/rule_card.dart new file mode 100644 index 0000000..075931f --- /dev/null +++ b/lib/src/features/notification_parsing/presentation/widgets/rule_card.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import '../../../../app/l10n/l10n.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../categories/domain/entities/category.dart'; +import '../../domain/entities/parse_rule.dart'; +import '../../domain/enums.dart'; + +/// Карточка правила в списке (§9.2 / §12.3). +class RuleCard extends StatelessWidget { + const RuleCard({ + super.key, + required this.rule, + required this.categoryById, + required this.onTap, + required this.onToggle, + }); + + final ParseRule rule; + final Map categoryById; + final VoidCallback onTap; + final ValueChanged onToggle; + + @override + Widget build(BuildContext context) { + final p = context.palette; + final l10n = context.l10n; + + final icon = switch (rule.kind) { + ParseRuleKind.merchantToCategory => Icons.storefront_outlined, + ParseRuleKind.senderToAccount => Icons.credit_card_outlined, + ParseRuleKind.ignore => Icons.block_outlined, + }; + + final categoryName = + rule.categoryId != null ? categoryById[rule.categoryId]?.name : null; + final target = [ + rule.merchantCanonical, + categoryName, + ].whereType().join(' · '); + + return Dismissible( + key: ValueKey(rule.id), + direction: DismissDirection.endToStart, + confirmDismiss: (_) async { + onToggle(!rule.enabled); + return false; + }, + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + color: p.cardSoft, + child: Icon( + rule.enabled ? Icons.toggle_off_outlined : Icons.toggle_on_outlined, + color: p.ink2), + ), + child: Opacity( + opacity: rule.enabled ? 1 : 0.5, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 22, color: p.ink2), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + rule.pattern, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: p.ink), + ), + if (target.isNotEmpty) ...[ + const SizedBox(height: 2), + Text('→ $target', + style: TextStyle(fontSize: 13, color: p.ink2)), + ], + const SizedBox(height: 4), + Text( + rule.enabled + ? l10n.ruleMatchesCount(rule.matchCount) + : '${l10n.ruleMatchesCount(rule.matchCount)} · ${l10n.ruleDisabled}', + style: TextStyle(fontSize: 12, color: p.ink2), + ), + ], + ), + ), + Text(rule.matchMode.name, + style: TextStyle(fontSize: 11, color: p.line2)), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/features/profile/presentation/screens/profile_screen.dart b/lib/src/features/profile/presentation/screens/profile_screen.dart index ae2b2f1..5ef21c8 100644 --- a/lib/src/features/profile/presentation/screens/profile_screen.dart +++ b/lib/src/features/profile/presentation/screens/profile_screen.dart @@ -61,6 +61,12 @@ class ProfileScreen extends ConsumerWidget { title: l10n.profileCategoriesTile, onTap: () => context.push(AppRoutes.categoriesList), ), + Container(height: 1, color: p.line), + _NavRow( + icon: Icons.notifications_active_outlined, + title: l10n.profileParsingTile, + onTap: () => context.push(AppRoutes.parsingSettings), + ), ], ), ), diff --git a/test/features/notification_parsing/application/inbox_controller_test.dart b/test/features/notification_parsing/application/inbox_controller_test.dart new file mode 100644 index 0000000..ceff4f1 --- /dev/null +++ b/test/features/notification_parsing/application/inbox_controller_test.dart @@ -0,0 +1,329 @@ +import 'package:flutter_riverpod/flutter_riverpod.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/application/inbox_controller.dart'; +import 'package:new_budget/src/features/notification_parsing/application/notification_parsing_providers.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/entities/account_binding.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'; +import 'package:new_budget/src/features/notification_parsing/domain/repositories/account_bindings_repository.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/repositories/parse_rules_repository.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/repositories/rule_candidates_repository.dart'; +import 'package:new_budget/src/features/transactions/application/transaction_providers.dart'; +import 'package:new_budget/src/features/transactions/domain/entities/transaction.dart'; +import 'package:new_budget/src/features/transactions/domain/repositories/transaction_repository.dart'; + +const _userId = 'u1'; +final _now = DateTime(2026, 5, 29, 14, 5); + +RawMessage _message() => RawMessage( + id: 'msg1', + userId: _userId, + packageName: 'ru.sberbankmobile', + body: 'Покупка 1240 ₽ Карта *3456 PYATEROCHKA', + receivedAt: _now, + dedupHash: 'h', + status: RawMessageStatus.inbox, + createdAt: _now, + ); + +ParseDraft _draft() => ParseDraft( + rawMessageId: 'msg1', + type: TransactionType.expense, + amount: 124000, + cardLast4: '3456', + merchantRaw: 'PYATEROCHKA', + kind: TxKind.purchase, + source: ParseSource.regex, + ); + +// ── Fakes ──────────────────────────────────────────────────────────────── + +class _FakeTransactionRepo implements TransactionRepository { + final List> created = []; + + @override + Future create({ + required String userId, + required String accountId, + String? categoryId, + required TransactionType type, + required int amount, + required DateTime date, + String? merchant, + String? extraInfo, + String? transferToAccountId, + String? rawMessageId, + bool autoApplied = false, + String? appliedByRuleId, + }) async { + created.add({ + 'accountId': accountId, + 'categoryId': categoryId, + 'amount': amount, + 'merchant': merchant, + 'rawMessageId': rawMessageId, + }); + return Transaction( + id: 'tx1', + userId: userId, + accountId: accountId, + categoryId: categoryId, + type: type, + amount: amount, + date: date, + merchant: merchant, + rawMessageId: rawMessageId, + autoApplied: autoApplied, + appliedByRuleId: appliedByRuleId, + createdAt: _now, + ); + } + + @override + dynamic noSuchMethod(Invocation invocation) => + throw UnimplementedError(invocation.memberName.toString()); +} + +class _FakeRawMessagesRepo implements RawMessagesRepository { + final List<(String, String)> linked = []; + final List<(String, RawMessageStatus)> statusUpdates = []; + + @override + Future linkTransaction(String id, String transactionId) async { + linked.add((id, transactionId)); + } + + @override + Future updateStatus(String id, RawMessageStatus status) async { + statusUpdates.add((id, status)); + } + + @override + dynamic noSuchMethod(Invocation invocation) => + throw UnimplementedError(invocation.memberName.toString()); +} + +class _FakeParseRulesRepo implements ParseRulesRepository { + final List> created = []; + + @override + Future create({ + required String userId, + required ParseRuleKind kind, + required MatchMode matchMode, + required String pattern, + int priority = 0, + String? merchantCanonical, + String? categoryId, + String? accountId, + }) async { + created.add({ + 'kind': kind, + 'matchMode': matchMode, + 'pattern': pattern, + 'merchantCanonical': merchantCanonical, + 'categoryId': categoryId, + }); + return ParseRule( + id: 'rule1', + userId: userId, + kind: kind, + matchMode: matchMode, + pattern: pattern, + merchantCanonical: merchantCanonical, + categoryId: categoryId, + accountId: accountId, + createdAt: _now, + ); + } + + @override + dynamic noSuchMethod(Invocation invocation) => + throw UnimplementedError(invocation.memberName.toString()); +} + +class _FakeRuleCandidatesRepo implements RuleCandidatesRepository { + final List<(String, String)> deleted = []; + final List> observed = []; + + @override + Future deleteByRawValue(String userId, String rawValue) async { + deleted.add((userId, rawValue)); + } + + @override + Future observe({ + required String userId, + required ParseRuleKind kind, + required String rawValue, + required String resolvedValue, + }) async { + observed.add({ + 'rawValue': rawValue, + 'resolvedValue': resolvedValue, + }); + } + + @override + dynamic noSuchMethod(Invocation invocation) => + throw UnimplementedError(invocation.memberName.toString()); +} + +class _FakeAccountBindingsRepo implements AccountBindingsRepository { + AccountBinding? existing; + final List> created = []; + + @override + Future findByPackageAndCard( + String userId, + String packageName, + String cardLast4, + ) async => + existing; + + @override + Future create({ + required String userId, + String? packageName, + String? bankKey, + String? cardLast4, + String? phone, + required String accountId, + }) async { + created.add({ + 'packageName': packageName, + 'cardLast4': cardLast4, + 'accountId': accountId, + }); + return AccountBinding( + id: 'b1', + userId: userId, + packageName: packageName, + cardLast4: cardLast4, + accountId: accountId, + createdAt: _now, + ); + } + + @override + dynamic noSuchMethod(Invocation invocation) => + throw UnimplementedError(invocation.memberName.toString()); +} + +void main() { + late _FakeTransactionRepo txRepo; + late _FakeRawMessagesRepo rawRepo; + late _FakeParseRulesRepo rulesRepo; + late _FakeRuleCandidatesRepo candidatesRepo; + late _FakeAccountBindingsRepo bindingsRepo; + late ProviderContainer container; + + setUp(() { + txRepo = _FakeTransactionRepo(); + rawRepo = _FakeRawMessagesRepo(); + rulesRepo = _FakeParseRulesRepo(); + candidatesRepo = _FakeRuleCandidatesRepo(); + bindingsRepo = _FakeAccountBindingsRepo(); + container = ProviderContainer( + overrides: [ + transactionRepositoryProvider.overrideWithValue(txRepo), + rawMessagesRepositoryProvider.overrideWithValue(rawRepo), + parseRulesRepositoryProvider.overrideWithValue(rulesRepo), + ruleCandidatesRepositoryProvider.overrideWithValue(candidatesRepo), + accountBindingsRepositoryProvider.overrideWithValue(bindingsRepo), + ], + ); + }); + + tearDown(() => container.dispose()); + + InboxController controller() => + container.read(inboxControllerProvider.notifier); + + group('createRule', () { + test('creates transaction + rule, removes candidate, links, binds', () async { + await controller().createRule( + userId: _userId, + message: _message(), + draft: _draft(), + accountId: 'acc1', + categoryId: 'cat1', + merchantCanonical: 'PYATEROCHKA', + pattern: 'PYATEROCHKA', + ); + + expect(txRepo.created, hasLength(1)); + expect(txRepo.created.single['categoryId'], 'cat1'); + expect(txRepo.created.single['rawMessageId'], 'msg1'); + + expect(rulesRepo.created, hasLength(1)); + expect(rulesRepo.created.single['kind'], ParseRuleKind.merchantToCategory); + expect(rulesRepo.created.single['categoryId'], 'cat1'); + + expect(candidatesRepo.deleted, contains((_userId, 'PYATEROCHKA'))); + expect(rawRepo.linked, contains(('msg1', 'tx1'))); + + // _maybeBind: no existing binding → creates one (card *3456 → acc1). + expect(bindingsRepo.created, hasLength(1)); + expect(bindingsRepo.created.single['cardLast4'], '3456'); + expect(bindingsRepo.created.single['accountId'], 'acc1'); + + expect(container.read(inboxControllerProvider).hasValue, isTrue); + }); + + test('does not re-create binding when one already exists', () async { + bindingsRepo.existing = AccountBinding( + id: 'old', + userId: _userId, + packageName: 'ru.sberbankmobile', + cardLast4: '3456', + accountId: 'acc1', + createdAt: _now, + ); + + await controller().createRule( + userId: _userId, + message: _message(), + draft: _draft(), + accountId: 'acc1', + categoryId: 'cat1', + merchantCanonical: 'PYATEROCHKA', + pattern: 'PYATEROCHKA', + ); + + expect(bindingsRepo.created, isEmpty); + }); + }); + + group('confirmOnce', () { + test('creates transaction without a rule, observes candidate', () async { + await controller().confirmOnce( + userId: _userId, + message: _message(), + draft: _draft(), + accountId: 'acc1', + categoryId: 'cat1', + ); + + expect(txRepo.created, hasLength(1)); + expect(rulesRepo.created, isEmpty); + expect(rawRepo.linked, contains(('msg1', 'tx1'))); + expect(candidatesRepo.observed, hasLength(1)); + expect(candidatesRepo.observed.single['resolvedValue'], 'cat1'); + }); + }); + + group('ignore', () { + test('sets message status to ignored, no transaction', () async { + await controller().ignore(_message()); + + expect(rawRepo.statusUpdates, contains(('msg1', RawMessageStatus.ignored))); + expect(txRepo.created, isEmpty); + expect(rulesRepo.created, isEmpty); + }); + }); +} diff --git a/test/features/notification_parsing/parser/confidence_scorer_test.dart b/test/features/notification_parsing/parser/confidence_scorer_test.dart new file mode 100644 index 0000000..d264992 --- /dev/null +++ b/test/features/notification_parsing/parser/confidence_scorer_test.dart @@ -0,0 +1,130 @@ +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/confidence_scorer.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'; + +RawMessage _msg(String body) => RawMessage( + id: 'm1', + userId: 'u1', + packageName: 'pkg', + body: body, + receivedAt: DateTime(2026, 5, 29), + dedupHash: 'h', + status: RawMessageStatus.pending, + createdAt: DateTime(2026, 5, 29), + ); + +ParseDraft _draft({ + String? merchantRaw = 'PYATEROCHKA', + int amount = 124000, + String currency = 'RUB', +}) => + ParseDraft( + rawMessageId: 'm1', + type: TransactionType.expense, + amount: amount, + currency: currency, + merchantRaw: merchantRaw, + source: ParseSource.regex, + ); + +ParseRule _rule({String? categoryId = 'c1'}) => ParseRule( + id: 'r1', + userId: 'u1', + kind: ParseRuleKind.merchantToCategory, + matchMode: MatchMode.contains, + pattern: 'PYATEROCHKA', + categoryId: categoryId, + createdAt: DateTime(2026, 1, 1), + ); + +void main() { + group('scoreDraft', () { + test('regex amount & type are 100', () { + final s = scoreDraft( + draft: _draft(), + message: _msg('Покупка PYATEROCHKA 1240 ₽ карта *3456'), + accountScore: 100, + ); + expect(s.amount, 100); + expect(s.type, 100); + expect(s.account, 100); + }); + + test('active rule → merchant & category 100', () { + final s = scoreDraft( + draft: _draft(), + message: _msg('Покупка PYATEROCHKA 1240 ₽ карта *3456'), + accountScore: 100, + merchantRule: _rule(), + ); + expect(s.merchant, 100); + expect(s.category, 100); + expect(s.otherFieldsMin, 100); + }); + + test('short merchant name is capped at 30', () { + final s = scoreDraft( + draft: _draft(merchantRaw: 'AB'), + message: _msg('Покупка AB 1240 ₽ карта *3456 что-то ещё длинное'), + accountScore: 100, + ); + expect(s.merchant, lessThanOrEqualTo(30)); + }); + + test('all-digit merchant capped at 20', () { + final s = scoreDraft( + draft: _draft(merchantRaw: '12345678'), + message: _msg('Покупка 12345678 1240 ₽ карта *3456 длинное тело тут'), + accountScore: 100, + ); + expect(s.merchant, lessThanOrEqualTo(20)); + }); + + test('category never exceeds merchant', () { + final s = scoreDraft( + draft: _draft(merchantRaw: 'AB'), + message: _msg('Покупка AB 1240 ₽ карта *3456 длинное тело сообщения'), + accountScore: 100, + ); + expect(s.category, lessThanOrEqualTo(s.merchant)); + }); + + test('short body caps amount/merchant/category at 50', () { + final s = scoreDraft( + draft: _draft(), + message: _msg('1240'), + accountScore: 100, + merchantRule: _rule(), + ); + expect(s.amount, lessThanOrEqualTo(50)); + expect(s.merchant, lessThanOrEqualTo(50)); + expect(s.category, lessThanOrEqualTo(50)); + }); + + test('empty currency caps amount at 40', () { + final s = scoreDraft( + draft: _draft(currency: ''), + message: _msg('Покупка PYATEROCHKA 1240 ₽ карта *3456 длинное тело'), + accountScore: 100, + ); + expect(s.amount, lessThanOrEqualTo(40)); + }); + }); + + group('looksLikeNonTransaction', () { + test('balance-only message → true', () { + expect(looksLikeNonTransaction('Доступно по карте 5000 ₽'), isTrue); + }); + + test('purchase message → false', () { + expect( + looksLikeNonTransaction('Покупка PYATEROCHKA Баланс 5000'), + isFalse, + ); + }); + }); +} diff --git a/test/features/notification_parsing/parser/decision_gate_test.dart b/test/features/notification_parsing/parser/decision_gate_test.dart new file mode 100644 index 0000000..8438927 --- /dev/null +++ b/test/features/notification_parsing/parser/decision_gate_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:new_budget/src/features/notification_parsing/data/parser/confidence_scorer.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_rule.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/enums.dart'; + +ParseRule _rule() => ParseRule( + id: 'r1', + userId: 'u1', + kind: ParseRuleKind.merchantToCategory, + matchMode: MatchMode.contains, + pattern: 'PYATEROCHKA', + categoryId: 'c1', + createdAt: DateTime(2026, 1, 1), + ); + +const _strongScores = FieldScores( + amount: 100, + account: 100, + type: 100, + merchant: 100, + category: 100, +); + +void main() { + group('decide', () { + test('rule + sanity + scores ≥ strictness → autoApply', () { + final d = decide( + merchantRule: _rule(), + sanityPassed: true, + scores: _strongScores, + strictness: 85, + amountMinor: 124000, + hasAccount: true, + ); + expect(d, GateDecision.autoApply); + }); + + test('no rule → inbox', () { + final d = decide( + merchantRule: null, + sanityPassed: true, + scores: _strongScores, + strictness: 85, + amountMinor: 124000, + hasAccount: true, + ); + expect(d, GateDecision.inbox); + }); + + test('scores below strictness → inbox', () { + final d = decide( + merchantRule: _rule(), + sanityPassed: true, + scores: const FieldScores( + amount: 100, + account: 45, + type: 100, + merchant: 100, + category: 100, + ), + strictness: 85, + amountMinor: 124000, + hasAccount: true, + ); + expect(d, GateDecision.inbox); + }); + + test('no account → inbox even with rule', () { + final d = decide( + merchantRule: _rule(), + sanityPassed: true, + scores: _strongScores, + strictness: 85, + amountMinor: 124000, + hasAccount: false, + ); + expect(d, GateDecision.inbox); + }); + + test('huge amount → inbox even with rule', () { + final d = decide( + merchantRule: _rule(), + sanityPassed: true, + scores: _strongScores, + strictness: 85, + amountMinor: 100001 * 100, + hasAccount: true, + ); + expect(d, GateDecision.inbox); + }); + + test('sanity failed → inbox', () { + final d = decide( + merchantRule: _rule(), + sanityPassed: false, + scores: _strongScores, + strictness: 85, + amountMinor: 124000, + hasAccount: true, + ); + expect(d, GateDecision.inbox); + }); + }); +} diff --git a/test/features/notification_parsing/parser/draft_codec_test.dart b/test/features/notification_parsing/parser/draft_codec_test.dart new file mode 100644 index 0000000..2ef52d7 --- /dev/null +++ b/test/features/notification_parsing/parser/draft_codec_test.dart @@ -0,0 +1,67 @@ +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/draft_codec.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_draft.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/entities/rule_suggestion.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/enums.dart'; + +void main() { + group('draft codec round-trip', () { + test('full draft + suggestion survives encode/decode', () { + final draft = ParseDraft( + rawMessageId: 'm1', + type: TransactionType.expense, + amount: 124000, + currency: 'RUB', + cardLast4: '3456', + merchantRaw: 'PYATEROCHKA 14', + counterpartyPhone: '+79991234567', + dateTime: DateTime(2026, 5, 29, 14, 5), + kind: TxKind.purchase, + accountId: 'a1', + merchantCanonical: 'PYATEROCHKA', + categoryId: 'c1', + source: ParseSource.regex, + ); + const sugg = RuleSuggestion( + merchantRaw: 'PYATEROCHKA 14', + merchantCanonical: 'PYATEROCHKA', + categoryId: 'c1', + categoryName: 'Продукты', + accountId: 'a1', + ); + + final decoded = decodeDraftBundle(encodeDraftBundle(draft, sugg)); + expect(decoded, isNotNull); + expect(decoded!.draft.amount, 124000); + expect(decoded.draft.type, TransactionType.expense); + expect(decoded.draft.kind, TxKind.purchase); + expect(decoded.draft.cardLast4, '3456'); + expect(decoded.draft.merchantRaw, 'PYATEROCHKA 14'); + expect(decoded.draft.dateTime, DateTime(2026, 5, 29, 14, 5)); + expect(decoded.draft.source, ParseSource.regex); + expect(decoded.suggestion, isNotNull); + expect(decoded.suggestion!.merchantCanonical, 'PYATEROCHKA'); + expect(decoded.suggestion!.categoryName, 'Продукты'); + }); + + test('draft without suggestion', () { + final draft = ParseDraft( + rawMessageId: 'm1', + type: TransactionType.income, + amount: 500000, + source: ParseSource.regex, + ); + final decoded = decodeDraftBundle(encodeDraftBundle(draft, null)); + expect(decoded, isNotNull); + expect(decoded!.suggestion, isNull); + expect(decoded.draft.type, TransactionType.income); + expect(decoded.draft.currency, 'RUB'); + }); + + test('null / empty json → null', () { + expect(decodeDraftBundle(null), isNull); + expect(decodeDraftBundle(''), isNull); + }); + }); +} diff --git a/test/features/notification_parsing/parser/misc_parser_test.dart b/test/features/notification_parsing/parser/misc_parser_test.dart new file mode 100644 index 0000000..90bb1e3 --- /dev/null +++ b/test/features/notification_parsing/parser/misc_parser_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:new_budget/src/features/notification_parsing/data/parser/dedup.dart'; +import 'package:new_budget/src/features/notification_parsing/data/parser/merchant_normalizer.dart'; + +void main() { + group('normalizeMerchant', () { + test('strips tail after star', () { + expect(normalizeMerchant('WBSPB*MOSCOW'), 'WBSPB'); + }); + + test('strips trailing point-of-sale digits', () { + expect(normalizeMerchant('PYATEROCHKA 2351'), 'PYATEROCHKA'); + }); + + test('collapses internal whitespace and trims', () { + expect(normalizeMerchant(' MAGNIT U '), 'MAGNIT U'); + }); + + test('falls back to trimmed raw if normalization empties it', () { + expect(normalizeMerchant(' 123 '), '123'); + }); + }); + + group('computeDedupHash', () { + test('is deterministic', () { + final a = computeDedupHash('pkg', 'Покупка 1240 ₽'); + final b = computeDedupHash('pkg', 'Покупка 1240 ₽'); + expect(a, b); + }); + + test('differs for different body', () { + final a = computeDedupHash('pkg', 'Покупка 1240 ₽'); + final b = computeDedupHash('pkg', 'Покупка 1250 ₽'); + expect(a, isNot(b)); + }); + + test('differs for different package', () { + final a = computeDedupHash('pkg1', 'same'); + final b = computeDedupHash('pkg2', 'same'); + expect(a, isNot(b)); + }); + + test('fixed-width 16-hex output', () { + final h = computeDedupHash('pkg', 'x'); + expect(h.length, 16); + expect(RegExp(r'^[0-9a-f]{16}$').hasMatch(h), isTrue); + }); + }); +} diff --git a/test/features/notification_parsing/parser/regex_parser_test.dart b/test/features/notification_parsing/parser/regex_parser_test.dart new file mode 100644 index 0000000..ffe7527 --- /dev/null +++ b/test/features/notification_parsing/parser/regex_parser_test.dart @@ -0,0 +1,100 @@ +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/regex_parser.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'; + +RawMessage _msg(String body, {String pkg = 'ru.sberbankmobile'}) { + final now = DateTime(2026, 5, 29, 14, 5); + return RawMessage( + id: 'm1', + userId: 'u1', + packageName: pkg, + body: body, + receivedAt: now, + dedupHash: 'h', + status: RawMessageStatus.pending, + createdAt: now, + ); +} + +void main() { + const parser = RegexParser(); + + group('parseAmountMinor', () { + test('plain integer → minor units', () { + expect(parseAmountMinor('1240'), 124000); + }); + + test('RU thousands separator + decimal comma', () { + expect(parseAmountMinor('1 240,50'), 124050); + }); + + test('dot decimal', () { + expect(parseAmountMinor('99.90'), 9990); + }); + + test('trailing dot in 3-digit group is NOT a decimal', () { + // «1.240» — точка как разделитель тысяч (3 цифры после) → 124000. + expect(parseAmountMinor('1.240'), 124000); + }); + + test('empty / garbage → null', () { + expect(parseAmountMinor(''), isNull); + expect(parseAmountMinor(' '), isNull); + }); + }); + + group('RegexParser.parse', () { + test('purchase amount→card→merchant (Сбер-подобный)', () { + final d = parser.parse( + _msg('Покупка 1240р Карта *3456 PYATEROCHKA 14:05 Баланс: 5000р'), + ); + expect(d, isNotNull); + expect(d!.type, TransactionType.expense); + expect(d.amount, 124000); + expect(d.cardLast4, '3456'); + expect(d.merchantRaw, 'PYATEROCHKA'); + expect(d.source, ParseSource.regex); + expect(d.kind, TxKind.purchase); + }); + + test('purchase card→amount→merchant (Тинькофф-подобный)', () { + final d = parser.parse( + _msg('Покупка. Карта *3456. 1 240 ₽. Пятёрочка. Доступно 5 000 ₽'), + ); + expect(d, isNotNull); + expect(d!.type, TransactionType.expense); + expect(d.amount, 124000); + expect(d.cardLast4, '3456'); + expect(d.merchantRaw, 'Пятёрочка'); + }); + + test('income (зачисление)', () { + final d = parser.parse(_msg('Зачисление 5000 ₽ Карта *3456 Зарплата')); + expect(d, isNotNull); + expect(d!.type, TransactionType.income); + expect(d.amount, 500000); + expect(d.cardLast4, '3456'); + }); + + test('SBP transfer by phone', () { + final d = parser.parse(_msg('Перевод 1000 ₽ на +79991234567 успешно')); + expect(d, isNotNull); + expect(d!.amount, 100000); + expect(d.counterpartyPhone, '+79991234567'); + }); + + test('refund → income', () { + final d = parser.parse(_msg('Возврат 1240 ₽ Карта *3456')); + expect(d, isNotNull); + expect(d!.type, TransactionType.income); + expect(d.amount, 124000); + }); + + test('non-matching body → null', () { + expect(parser.parse(_msg('Ваш баланс по карте *3456 составляет 5000 ₽')), + isNull); + }); + }); +} diff --git a/test/features/notification_parsing/parser/rule_lookup_test.dart b/test/features/notification_parsing/parser/rule_lookup_test.dart new file mode 100644 index 0000000..3c78399 --- /dev/null +++ b/test/features/notification_parsing/parser/rule_lookup_test.dart @@ -0,0 +1,124 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:new_budget/src/features/notification_parsing/data/parser/rule_lookup.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'; + +ParseRule _rule({ + required String id, + required String pattern, + MatchMode mode = MatchMode.contains, + ParseRuleKind kind = ParseRuleKind.merchantToCategory, + bool enabled = true, + int matchCount = 0, + int priority = 0, + String? categoryId = 'c1', +}) { + return ParseRule( + id: id, + userId: 'u1', + kind: kind, + matchMode: mode, + pattern: pattern, + enabled: enabled, + matchCount: matchCount, + priority: priority, + categoryId: categoryId, + createdAt: DateTime(2026, 1, 1), + ); +} + +void main() { + group('patternMatches', () { + test('contains is case-insensitive over body', () { + expect( + patternMatches('pyaterochka', MatchMode.contains, + body: 'Покупка PYATEROCHKA 1240'), + isTrue, + ); + }); + + test('exact matches normalized merchant only', () { + expect( + patternMatches('pyaterochka', MatchMode.exact, + body: 'long body', merchantRaw: 'PYATEROCHKA'), + isTrue, + ); + expect( + patternMatches('pyat', MatchMode.exact, + body: 'long body', merchantRaw: 'PYATEROCHKA'), + isFalse, + ); + }); + + test('regex match, invalid regex → false', () { + expect(patternMatches(r'\d{4}', MatchMode.regex, body: 'Карта 3456'), + isTrue); + expect(patternMatches('[invalid(', MatchMode.regex, body: 'x'), isFalse); + }); + + test('empty pattern → false', () { + expect(patternMatches(' ', MatchMode.contains, body: 'anything'), + isFalse); + }); + }); + + group('findMerchantRule', () { + const body = 'Покупка PYATEROCHKA 1240'; + + test('returns matching enabled merchant rule', () { + final r = findMerchantRule( + [_rule(id: 'a', pattern: 'PYATEROCHKA')], + body: body, + ); + expect(r?.id, 'a'); + }); + + test('ignores disabled rules', () { + final r = findMerchantRule( + [_rule(id: 'a', pattern: 'PYATEROCHKA', enabled: false)], + body: body, + ); + expect(r, isNull); + }); + + test('ignores ignore-kind rules', () { + final r = findMerchantRule( + [_rule(id: 'a', pattern: 'PYATEROCHKA', kind: ParseRuleKind.ignore)], + body: body, + ); + expect(r, isNull); + }); + + test('conflict: longer pattern wins', () { + final r = findMerchantRule( + [ + _rule(id: 'short', pattern: 'PYAT'), + _rule(id: 'long', pattern: 'PYATEROCHKA'), + ], + body: body, + ); + expect(r?.id, 'long'); + }); + + test('conflict: equal length → higher matchCount wins', () { + final r = findMerchantRule( + [ + _rule(id: 'a', pattern: 'PYATEROCHKA', matchCount: 1), + _rule(id: 'b', pattern: 'PYATEROCHKX', matchCount: 9), + ], + body: 'PYATEROCHKA PYATEROCHKX', + ); + expect(r?.id, 'b'); + }); + }); + + group('findIgnoreRule', () { + test('finds enabled ignore rule', () { + final r = findIgnoreRule( + [_rule(id: 'i', pattern: 'спам', kind: ParseRuleKind.ignore)], + body: 'это спам реклама', + ); + expect(r?.id, 'i'); + }); + }); +} diff --git a/test/features/notification_parsing/presentation/inbox_card_test.dart b/test/features/notification_parsing/presentation/inbox_card_test.dart new file mode 100644 index 0000000..54c235a --- /dev/null +++ b/test/features/notification_parsing/presentation/inbox_card_test.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:new_budget/l10n/app_localizations.dart'; +import 'package:new_budget/src/app/theme/app_theme.dart'; +import 'package:new_budget/src/core/database/converters/enum_converters.dart'; +import 'package:new_budget/src/features/categories/domain/entities/category.dart'; +import 'package:new_budget/src/features/notification_parsing/application/inbox_controller.dart'; +import 'package:new_budget/src/features/notification_parsing/data/parser/draft_codec.dart'; +import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_draft.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'; +import 'package:new_budget/src/features/notification_parsing/presentation/widgets/inbox_card.dart'; + +final _now = DateTime(2026, 5, 29, 14, 5); + +class FakeInboxController extends InboxController { + int createRuleCalls = 0; + int confirmOnceCalls = 0; + int ignoreCalls = 0; + String? lastCreateRuleCategory; + + @override + AsyncValue build() => const AsyncData(null); + + @override + Future createRule({ + required String userId, + required RawMessage message, + required ParseDraft draft, + required String accountId, + String? categoryId, + required String merchantCanonical, + required String pattern, + MatchMode matchMode = MatchMode.contains, + bool bindAccount = true, + }) async { + createRuleCalls++; + lastCreateRuleCategory = categoryId; + } + + @override + Future confirmOnce({ + required String userId, + required RawMessage message, + required ParseDraft draft, + required String accountId, + String? categoryId, + bool bindAccount = true, + }) async { + confirmOnceCalls++; + } + + @override + Future ignore(RawMessage message) async { + ignoreCalls++; + } +} + +RawMessage _recognizedMessage() { + final draft = ParseDraft( + rawMessageId: 'msg1', + type: TransactionType.expense, + amount: 124000, + cardLast4: '3456', + merchantRaw: 'PYATEROCHKA', + merchantCanonical: 'PYATEROCHKA', + categoryId: 'cat1', + kind: TxKind.purchase, + source: ParseSource.regex, + ); + return RawMessage( + id: 'msg1', + userId: 'u1', + packageName: 'ru.sberbankmobile', + body: 'Покупка 1240 ₽ Карта *3456 PYATEROCHKA', + receivedAt: _now, + dedupHash: 'h', + status: RawMessageStatus.inbox, + draftJson: encodeDraftBundle(draft, null), + createdAt: _now, + ); +} + +Widget _host(FakeInboxController fake, RawMessage message) { + return ProviderScope( + overrides: [ + inboxControllerProvider.overrideWith(() => fake), + ], + child: MaterialApp( + theme: AppTheme.light(), + locale: const Locale('ru'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: InboxCard( + message: message, + userId: 'u1', + defaultAccountId: 'acc1', + categoryById: const { + 'cat1': Category( + id: 'cat1', + userId: 'u1', + name: 'Продукты', + type: CategoryType.expense, + archived: false, + ), + }, + ), + ), + ), + ); +} + +void main() { + setUpAll(() { + GoogleFonts.config.allowRuntimeFetching = false; + }); + + late FakeInboxController fake; + setUp(() => fake = FakeInboxController()); + + testWidgets('renders merchant, category and amount', (tester) async { + await tester.pumpWidget(_host(fake, _recognizedMessage())); + await tester.pump(); + + expect(find.text('PYATEROCHKA'), findsOneWidget); + expect(find.textContaining('Продукты'), findsWidgets); + expect(find.textContaining('240'), findsWidgets); // money formatted + }); + + testWidgets('tap "create rule" calls controller with category', (tester) async { + await tester.pumpWidget(_host(fake, _recognizedMessage())); + await tester.pump(); + + // Главная часть кнопки = ярлык «Правило: … → …». Категория задана → + // createRule вызывается напрямую, без открытия редактора. + final l10n = await AppLocalizations.delegate.load(const Locale('ru')); + await tester.tap(find.text(l10n.inboxCreateRule('PYATEROCHKA', 'Продукты'))); + await tester.pump(); + + expect(fake.createRuleCalls, 1); + expect(fake.lastCreateRuleCategory, 'cat1'); + }); + + testWidgets('tap confirm-once calls controller', (tester) async { + await tester.pumpWidget(_host(fake, _recognizedMessage())); + await tester.pump(); + + await tester.tap(find.byIcon(Icons.check)); + await tester.pump(); + + expect(fake.confirmOnceCalls, 1); + expect(fake.createRuleCalls, 0); + }); + + testWidgets('tap ignore calls controller', (tester) async { + await tester.pumpWidget(_host(fake, _recognizedMessage())); + await tester.pump(); + + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + + expect(fake.ignoreCalls, 1); + }); + + testWidgets('unrecognized message shows manual-add fallback', (tester) async { + final msg = _recognizedMessage().copyWith(draftJson: null); + await tester.pumpWidget(_host(fake, msg)); + await tester.pump(); + + final l10n = await AppLocalizations.delegate.load(const Locale('ru')); + expect(find.text(l10n.inboxUnrecognized), findsOneWidget); + expect(find.text(l10n.inboxAddManually), findsOneWidget); + }); +}