# Workflow определения счёта при парсинге уведомлений > ⚠️ **Реализовано и затем упрощено (2026-07):** описанная здесь модель привязок > `account_bindings` (карта/телефон → счёт, per-app default со звёздочкой) удалена в > schemaVersion 2. Действующая модель: правило `senderToAccount` → > `source_apps.defaultAccountId` → глобальный дефолт (не trusted). Документ — история. ## 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) разрешить авто-применение с дефолтным счётом. **Решения пользователя:** 1. Список приложений — **захардкодить каталог** сейчас, нативную выборку позже. 2. **Allowlist**: парсятся только включённые приложения-источники; остальные игнорируются. 3. Default-счёт: **per-app default + глобальный fallback** (`isDefault` в `account_bindings` + `Account.isDefault`). 4. Правила: **композитные раздельные** — задействовать `senderToAccount` + оставить `merchantToCategory`. 5. Gate: **авто-применять с дефолтным счётом** (но неоднозначность из нескольких реальных привязок без флага default — в Inbox). 6. **Сделать 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) + регистрация в `@DriftDatabase` tables. - **`app_database.dart`** — `schemaVersion` 6 → **7**; в `onUpgrade`: `if (from < 7) { await m.addColumn(accountBindingsTable, accountBindingsTable.isDefault); await m.createTable(sourceApps); }`. - **`account_binding.dart`** entity + **`account_binding_mapper.dart`** — пробросить `isDefault`. - **`account_bindings_dao.dart`** — добавить `findDefaultByPackageName(...)` и `setDefault(id, userId, packageName)` (сбросить флаг у остальных привязок пакета, выставить у выбранной). - **`account_bindings_repository.dart`**(+impl) — новые методы + `isDefault` в `create(...)`. - **Новые `source_apps` DAO / 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`. ### 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 + имя). Watch `enabledSourcePackagesProvider` / стрим `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`** (после «Правила»), icon `Icons.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.dart` - `application/parsing_worker.dart` (allowlist + проводка резолвера) - `data/source_apps/source_apps_catalog.dart` (новый) + `data/drift/tables/source_apps_table.dart` (новый) + DAO/entity/mapper/repo - `data/drift/tables/account_bindings_table.dart` + `core/database/app_database.dart` (миграция v7) - `data/drift/daos/account_bindings_dao.dart`, mapper, repository(+impl), entity - `presentation/screens/source_apps_screen.dart`, `app_bindings_screen.dart` (новые) + router/routes/настройки - `presentation/screens/{rules_list,rule_editor}_screen.dart`, `widgets/rule_card.dart` - `application/{source_apps_controller,account_bindings_controller}.dart` (новые) + `notification_parsing_providers.dart` ## Верификация 1. `dart run build_runner build --delete-conflicting-outputs` && `flutter gen-l10n` && `flutter analyze` — чисто. 2. **Unit `account_resolver_test.dart`**: 7 случаев из таблицы B — `accountId`/`trusted`/`score`. 3. **Unit `decision_gate_test.dart`**: правило категории + `accountTrusted=true` → `autoApply`; `accountTrusted=false` (multi-binding) → `inbox`; нет счёта → `inbox`. 4. **Allowlist-тест** (`parsing_worker`): сообщение от пакета НЕ из включённых → `ignored`, AI не вызывается; от включённого банка без карты, но с глобальным дефолтом + правилом категории → авто-применение на дефолт. (Устарело: `pendingMessagesProvider` удалён — воркер подписан на Drift-стримы репозитория напрямую, в тестах достаточно `container.read(parsingWorkerProvider(userId))`.) 5. Существующий пакет `test/features/notification_parsing/` — зелёный. 6. Ручная проверка (`flutter run`): добавить приложение из каталога → включить → задать привязку к счёту и дефолт; убедиться, что уведомление от не-включённого пакета игнорируется; создать `senderToAccount`-правило.