Add per-app rules, transfer pairing, self-merchant flag, two-step onboarding

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>
This commit is contained in:
2026-07-10 22:49:04 +03:00
co-authored by Claude Opus 4.8
parent 1ac68c0eb1
commit da65bf6f8e
123 changed files with 5943 additions and 1246 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="new_budget"
android:label="Kitty finances"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -27,11 +27,25 @@ class NotificationIngestService : NotificationListenerService() {
val extras = notification.extras ?: return
val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()
// big-text полнее обычного text — берём его при наличии.
val body = (extras.getCharSequence(Notification.EXTRA_BIG_TEXT)
val extrasBody = (extras.getCharSequence(Notification.EXTRA_BIG_TEXT)
?: extras.getCharSequence(Notification.EXTRA_TEXT))
?.toString()
?.trim()
.orEmpty()
// ВТБ прячет текст операции: в EXTRA_TEXT кладёт заглушку
// «Конфиденциальная информация в уведомлении скрыта» (реальный контент
// рисуется в custom RemoteViews и в extras не попадает), а полный текст
// («Списание 2000р Счет*3891 …») доступен только в tickerText. Второго
// поста с текстом, как у Т-Банка, ВТБ не шлёт — поэтому при пустом body
// или заглушке падаем обратно на ticker.
val ticker = notification.tickerText?.toString()?.trim().orEmpty()
val body = if ((extrasBody.isEmpty() || isHiddenContentStub(extrasBody)) &&
ticker.isNotEmpty()
) {
ticker
} else {
extrasBody
}
val diagnosticMode = store.isDiagnosticMode()
@@ -51,6 +65,12 @@ class NotificationIngestService : NotificationListenerService() {
LiveSink.tick()
}
/** Заглушка «содержимое скрыто» вместо текста операции (ВТБ и т.п.). */
private fun isHiddenContentStub(body: String): Boolean {
val lower = body.lowercase()
return lower.contains("конфиденциальн") && lower.contains("скрыт")
}
/**
* Человекочитаемый дамп уведомления для режима диагностики: `key`,
* `postTime`, `visibility`, `flags`, `tickerText` и все поля `extras`.
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="notif_listener_label">NewBudget — распознавание уведомлений</string>
<string name="notif_listener_label">Kitty finances — распознавание уведомлений</string>
</resources>