Files
OnBudget/docs/account_determination_plan.md
SandersandClaude Opus 4.8 4f99b80169 Replace account_bindings with per-app default account; build analytics charts
Notification parsing:
- Drop the account_bindings table/DAO/repo/entity/controller/screen; account
  resolution now goes senderToAccount rule -> source_apps.defaultAccountId
  (trusted) -> global default (untrusted -> Inbox), via v1->v2 migration.
- Add defaultAccountId to source_apps; per-app settings consolidated into
  source_app_detail_screen (/settings/parsing/apps/:pkg).
- Inbox auto-learns an app default on first Confirm/CreateRule; account picker
  on the card instead of a disabled button; parse_error_labels extracted.

Analytics:
- Replace placeholder screen with fl_chart cards (chart_card, chart_theme,
  month_stepper, month_math domain helper); slim down habit_analysis_screen.

Android: add launcher icon (adaptive foreground + colors.xml) and app_name.

Tests: migration_v2, analytics (screen/month_math), AI retry, inbox
visibility; update resolver/gate/inbox suites for the new resolution path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:09:12 +03:00

13 KiB
Raw Permalink Blame History

Workflow определения счёта при парсинге уведомлений

⚠️ Реализовано и затем упрощено (2026-07): описанная здесь модель привязок account_bindings (карта/телефон → счёт, per-app default со звёздочкой) удалена в schemaVersion 2. Действующая модель: правило senderToAccountsource_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.dartconst список 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.dartBoolColumn get isDefault => boolean().withDefault(const Constant(false))();
  • Новая source_apps_table.dart (см. раздел A) + регистрация в @DriftDatabase tables.
  • app_database.dartschemaVersion 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.dartfindSenderRule(rules, body, merchantRaw) по образцу findMerchantRule, kind == ParseRuleKind.senderToAccount.
  • account_resolver.dartAccountResolution + bool trusted (и опц. AccountSource source); resolve(...) переписать по таблице B; новые параметры body, merchantRaw, senderRules, globalDefaultAccountId.
  • confidence_scorer.dart — убрать account из FieldScores.otherFieldsMin (min только amount, type); account-score остаётся для подсветки «?».
  • decision_gate.dartdecide(...): вместо 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=trueautoApply; 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-правило.