Notification parsing: - Per-app parse rules (parse_rules.packageName; getEnabledForApp, NULL = legacy global) - Transfer pairing: transfer_pair_matcher + transfer_pairing_blocklist table/dao/repo - source_apps.selfMerchant flag (Ozon inbox rework: default account picker, suppress AI category prefill + rule suggestion) - raw_messages.diagnostics dump captured under diagnostic-mode toggle - Inbox card / settings / log UI reworks Onboarding: - Two-step flow (name -> first account); UserSeeder seeds categories only, no accounts Schema bumped to v11; drop obsolete migration + mixed-merchant tests, add new coverage. Add ios/ platform folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Workflow определения счёта при парсинге уведомлений
Context
Приложение парсит уведомления банков и создаёт черновики транзакций. Проблемы по текущему коду:
- Нет управляемого списка приложений-источников.
packageNameприходит как свободный текст (в debug —debug_inject_screen.dart; нативногоNotificationListenerServiceещё нет, Phase 0). Парсится всё подряд. Каталога банков нет (bank_templates_catalog— только в комментарии схемы). - Транзакция не создаётся без счёта.
decision_gate.dart:27:if (!hasAccount) return inbox;. Даже когда правило категории сработало. AccountResolver(account_resolver.dart) умеет карта→счёт, телефон→счёт, packageName→счёт; при нескольких счетах в банке без распознанной карты берёт «первый» (score 45). Нет понятия умолчательного счёта.senderToAccountобъявлен (enums.dart:25), есть в UI (rules_list_screen.dart:33,rule_card.dart:31), но не используется в пайплайне — мёртвый код.- Правило-мерчант кладёт
accountIdлишь как fallback (parsing_worker.dart:265). - Привязки (
account_bindings) создаются неявно и не управляемы из UI.
Цель: (1) список приложений-источников (allowlist) — парсить только выбранные; (2) на базе
этого списка задавать привязку приложение→счёт с умолчательными счетами (per-app + глобальный);
(3) задействовать senderToAccount-правила; (4) разрешить авто-применение с дефолтным счётом.
Решения пользователя:
- Список приложений — захардкодить каталог сейчас, нативную выборку позже.
- Allowlist: парсятся только включённые приложения-источники; остальные игнорируются.
- Default-счёт: per-app default + глобальный fallback (
isDefaultвaccount_bindings+Account.isDefault). - Правила: композитные раздельные — задействовать
senderToAccount+ оставитьmerchantToCategory. - Gate: авто-применять с дефолтным счётом (но неоднозначность из нескольких реальных привязок без флага default — в Inbox).
- Сделать UI управления приложениями-источниками и их привязками.
A. Список приложений-источников (allowlist)
Каталог (хардкод). Новый data/source_apps/source_apps_catalog.dart — const список
SourceAppTemplate(packageName, displayName, bankKey?) для известных банков РФ
(Сбер ru.sberbankmobile, Т-Банк com.idamob.tinkoff.android, Альфа, ВТБ, Газпром и т.п.).
Это лишь источник вариантов для выбора; позже заменится нативным списком установленных приложений.
Хранение выбора (per-user). Новая Drift-таблица source_apps:
id, userId(FK), packageName, displayName(nullable), enabled(bool, default true), createdAt,
unique (userId, packageName). Присутствие строки = приложение добавлено; enabled — мониторится
ли сейчас. Заводится при добавлении из каталога или вручную (кастомный packageName).
Слой: table + DAO + entity (freezed) + mapper + repository(+impl), по образцу account_bindings.
Allowlist-фильтр в пайплайне. В parsing_worker.dart перед разбором: загрузить множество
включённых packageName пользователя; если msg.packageName не в нём — пометить
RawMessageStatus.ignored и выйти (до AI/правил, чтобы не тратить токены). Провайдер
enabledSourcePackagesProvider(userId) (Set) — @riverpod.
B. Цепочка определения счёта (итоговая)
В AccountResolver.resolve(...), который теперь принимает также body, merchantRaw,
senderRules и globalDefaultAccountId. Порядок по убыванию доверия:
| # | Источник | accountId | trusted | score |
|---|---|---|---|---|
| 1 | binding packageName+cardLast4 / phone |
да | да | 100 |
| 2 | senderToAccount-правило (матч по телу) |
да | да | 90 |
| 3 | единственный binding по packageName |
да | да | 75 |
| 4 | per-app default binding (isDefault, card неизвестна) |
да | да | 70 |
| 5 | несколько bindings без default-флага | первый | нет → Inbox | 45 |
| 6 | глобальный Account.isDefault |
да | да | 40 |
| 7 | ничего | null | — → Inbox | 15 |
trusted — новое булево в AccountResolution; именно оно (а не числовой score) решает судьбу
в gate: неоднозначность (#5) остаётся в Inbox, осознанные дефолты (#4, #6) разрешают авто-применение.
Изменения по слоям
1. Schema / data (Drift) — миграция v6 → v7
account_bindings_table.dart—BoolColumn get isDefault => boolean().withDefault(const Constant(false))();- Новая
source_apps_table.dart(см. раздел A) + регистрация в@DriftDatabasetables. app_database.dart—schemaVersion6 → 7; вonUpgrade:if (from < 7) { await m.addColumn(accountBindingsTable, accountBindingsTable.isDefault); await m.createTable(sourceApps); }.account_binding.dartentity +account_binding_mapper.dart— проброситьisDefault.account_bindings_dao.dart— добавитьfindDefaultByPackageName(...)иsetDefault(id, userId, packageName)(сбросить флаг у остальных привязок пакета, выставить у выбранной).account_bindings_repository.dart(+impl) — новые методы +isDefaultвcreate(...).- Новые
source_appsDAO / entity / mapper / repository(+impl) + провайдеры вnotification_parsing_providers.dart.
После правок таблиц/entity:
dart run build_runner build --delete-conflicting-outputs.
2. Резолвер счёта + правила (data/parser)
rule_lookup.dart—findSenderRule(rules, body, merchantRaw)по образцуfindMerchantRule,kind == ParseRuleKind.senderToAccount.account_resolver.dart—AccountResolution+bool trusted(и опц.AccountSource source);resolve(...)переписать по таблице B; новые параметрыbody,merchantRaw,senderRules,globalDefaultAccountId.confidence_scorer.dart— убратьaccountизFieldScores.otherFieldsMin(min толькоamount,type); account-score остаётся для подсветки «?».decision_gate.dart—decide(...): вместоhasAccountприниматьaccountResolvedиaccountTrusted. Логика:if (!accountResolved || !accountTrusted) return inbox;далее как раньше.
3. Pipeline (application)
parsing_worker.dart(_runPipeline):- в начале — allowlist-фильтр (раздел A): не в множестве →
ignored, выход. - грузить
rulesраньше; передатьsenderRules/body/merchantRaw+globalDefaultAccountId(accountRepositoryProvider.watchDefault(userId).first) вresolver.resolve(...). - в
decide(...)передаватьaccountResolved: draft.accountId != null,accountTrusted: resolution.trusted.
- в начале — allowlist-фильтр (раздел A): не в множестве →
4. UI приложений-источников и привязок (presentation)
- Routes в
app_routes.dart:parsingApps = '/settings/parsing/apps',parsingAppBindings = '/settings/parsing/apps/:pkg'; регистрация вapp_router.dart. source_apps_screen.dart(новый): секция «Включённые» (мониторятся, toggle + chevron → привязки), секция «Доступные» (из каталога, ещё не добавлены — тап включает), «Добавить вручную» (кастомный packageName + имя). WatchenabledSourcePackagesProvider/ стримsource_apps.app_bindings_screen.dart(новый, per-app): привязки выбранногоpackageName— список (card/phone → счёт), флаг «по умолчанию для приложения» (dao.setDefault), добавить привязку (счёт + опц. cardLast4), удалить (deleteById).- Контроллеры
source_apps_controller.dartиaccount_bindings_controller.dart(тонкие@riverpod, по образцуrules_controller.dart). - Вход:
_NavTile«Приложения-источники» вparsing_settings_screen.dart(после «Правила»), iconIcons.apps_outlined.
5. UI правил — создание senderToAccount
rules_list_screen.dart:129+rule_editor_screen.dart— выборkind(merchant→category | sender→account); дляsenderToAccountпоказывать только picker счёта.rule_card.dart— отображать имя счёта дляsenderToAccount.
6. Локализация
- В
app_en.arb/app_ru.arb— ключи для экранов приложений-источников/привязок, выбора kind правила (ruleKindMerchant/ruleKindAccount), флага default, добавления/удаления. Затемflutter gen-l10n.
Критические файлы
data/parser/account_resolver.dart,decision_gate.dart,confidence_scorer.dart,rule_lookup.dartapplication/parsing_worker.dart(allowlist + проводка резолвера)data/source_apps/source_apps_catalog.dart(новый) +data/drift/tables/source_apps_table.dart(новый) + DAO/entity/mapper/repodata/drift/tables/account_bindings_table.dart+core/database/app_database.dart(миграция v7)data/drift/daos/account_bindings_dao.dart, mapper, repository(+impl), entitypresentation/screens/source_apps_screen.dart,app_bindings_screen.dart(новые) + router/routes/настройкиpresentation/screens/{rules_list,rule_editor}_screen.dart,widgets/rule_card.dartapplication/{source_apps_controller,account_bindings_controller}.dart(новые) +notification_parsing_providers.dart
Верификация
dart run build_runner build --delete-conflicting-outputs&&flutter gen-l10n&&flutter analyze— чисто.- Unit
account_resolver_test.dart: 7 случаев из таблицы B —accountId/trusted/score. - Unit
decision_gate_test.dart: правило категории +accountTrusted=true→autoApply;accountTrusted=false(multi-binding) →inbox; нет счёта →inbox. - Allowlist-тест (
parsing_worker): сообщение от пакета НЕ из включённых →ignored, AI не вызывается; от включённого банка без карты, но с глобальным дефолтом + правилом категории → авто-применение на дефолт. (Устарело:pendingMessagesProviderудалён — воркер подписан на Drift-стримы репозитория напрямую, в тестах достаточноcontainer.read(parsingWorkerProvider(userId)).) - Существующий пакет
test/features/notification_parsing/— зелёный. - Ручная проверка (
flutter run): добавить приложение из каталога → включить → задать привязку к счёту и дефолт; убедиться, что уведомление от не-включённого пакета игнорируется; создатьsenderToAccount-правило.