Two fixes for the notification-parsing pipeline: - CompanionDeviceManager pairing (DEVICE_PROFILE_WATCH -> COMPANION_DEVICE_WATCH role -> RECEIVE_SENSITIVE_NOTIFICATIONS) via a new platform channel in MainActivity.kt, companion_device_channel.dart and companion_access_controller, surfaced as a card in parsing settings. Lifts the system "Confidential" redaction that hid VTB/T-Bank notification text from the listener. - raw_messages.ai_response: the model's raw completion content is now persisted (schema v4 + migration) and shown in the parsing log detail panel. Unlike draftJson it survives inbox sweeps and is filled even for partial/ignored outcomes. flutter analyze: no issues. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
5.3 KiB
Dart
147 lines
5.3 KiB
Dart
import 'package:drift/native.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:new_budget/src/core/database/app_database.dart';
|
|
import 'package:new_budget/src/core/database/converters/enum_converters.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
|
|
|
|
/// Миграция v2 → v3: единая модель правил (см. `AppDatabase._migrateV2ToV3`) —
|
|
/// колонка `kind` дропается table rewrite'ом, появляются `is_ignore` и
|
|
/// `auto_apply`; kind='ignore' → is_ignore=1, остальные данные целы.
|
|
///
|
|
/// Сценарий: raw-схема v2 + по правилу каждого старого kind заливаются в
|
|
/// `setup:` (до drift), `PRAGMA user_version = 2` заставляет drift выполнить
|
|
/// onUpgrade при открытии.
|
|
|
|
const _userId = 'u1';
|
|
const _sber = 'ru.sberbankmobile';
|
|
|
|
void _createV2Schema(dynamic raw) {
|
|
raw.execute('''
|
|
CREATE TABLE source_apps (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
display_name TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
self_merchant INTEGER NOT NULL DEFAULT 0,
|
|
default_account_id TEXT,
|
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
UNIQUE (user_id, package_name)
|
|
);''');
|
|
raw.execute('''
|
|
CREATE TABLE parse_rules (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
package_name TEXT,
|
|
kind TEXT NOT NULL,
|
|
match_mode TEXT NOT NULL DEFAULT 'contains',
|
|
pattern TEXT NOT NULL,
|
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
match_count INTEGER NOT NULL DEFAULT 0,
|
|
weight INTEGER NOT NULL DEFAULT 1,
|
|
last_match_at INTEGER,
|
|
tx_type TEXT,
|
|
merchant_canonical TEXT,
|
|
category_id TEXT,
|
|
account_id TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
);''');
|
|
|
|
// По правилу каждого старого kind.
|
|
raw.execute(
|
|
'INSERT INTO parse_rules '
|
|
'(id, user_id, package_name, kind, match_mode, pattern, priority, '
|
|
'match_count, tx_type, merchant_canonical, category_id, account_id, '
|
|
'enabled) VALUES '
|
|
"('r-merchant', '$_userId', '$_sber', 'merchantToCategory', 'contains', "
|
|
"'PYATEROCHKA', 1, 5, 'expense', 'Пятёрочка', 'cat-1', NULL, 1), "
|
|
"('r-sender', '$_userId', '$_sber', 'senderToAccount', 'contains', "
|
|
"'*3456', 0, 2, NULL, NULL, NULL, 'acc-1', 1), "
|
|
"('r-ignore', '$_userId', '$_sber', 'ignore', 'regex', "
|
|
"'заказ \\d+', 0, 0, NULL, NULL, NULL, NULL, 0)");
|
|
|
|
// Хвост цепочки (v3→v4) добавляет колонку в raw_messages — в реальной
|
|
// v2-БД таблица есть, здесь достаточно её минимальной схемы.
|
|
raw.execute('''
|
|
CREATE TABLE raw_messages (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
title TEXT,
|
|
body TEXT NOT NULL,
|
|
received_at INTEGER NOT NULL,
|
|
dedup_hash TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
parse_attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
last_parse_error TEXT,
|
|
draft_json TEXT,
|
|
diagnostics TEXT,
|
|
confidence_amount INTEGER,
|
|
confidence_account INTEGER,
|
|
confidence_type INTEGER,
|
|
confidence_merchant INTEGER,
|
|
confidence_category INTEGER,
|
|
transaction_id TEXT,
|
|
paired_with_id TEXT,
|
|
pair_deadline INTEGER,
|
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
);''');
|
|
|
|
raw.execute('PRAGMA user_version = 2');
|
|
}
|
|
|
|
void main() {
|
|
late AppDatabase db;
|
|
|
|
setUp(() {
|
|
db = AppDatabase.forTesting(NativeDatabase.memory(setup: _createV2Schema));
|
|
});
|
|
|
|
tearDown(() => db.close());
|
|
|
|
test('колонка kind исчезла из parse_rules', () async {
|
|
final cols = await db
|
|
.customSelect("PRAGMA table_info('parse_rules')")
|
|
.get();
|
|
final names = cols.map((r) => r.read<String>('name')).toSet();
|
|
expect(names, isNot(contains('kind')));
|
|
expect(names, containsAll(['is_ignore', 'auto_apply', 'tx_type']));
|
|
});
|
|
|
|
test('merchantToCategory → классифицирующее правило, данные целы', () async {
|
|
final r = await db.parseRulesDao.findById('r-merchant');
|
|
expect(r, isNotNull);
|
|
expect(r!.isIgnore, isFalse);
|
|
expect(r.autoApply, isTrue, reason: 'дефолт auto_apply=1');
|
|
expect(r.pattern, 'PYATEROCHKA');
|
|
expect(r.matchMode, MatchMode.contains);
|
|
expect(r.priority, 1);
|
|
expect(r.matchCount, 5);
|
|
expect(r.txTypeGuard, TransactionType.expense);
|
|
expect(r.merchantCanonical, 'Пятёрочка');
|
|
expect(r.categoryId, 'cat-1');
|
|
expect(r.accountId, isNull);
|
|
expect(r.enabled, isTrue);
|
|
});
|
|
|
|
test('senderToAccount → правило со счётом', () async {
|
|
final r = await db.parseRulesDao.findById('r-sender');
|
|
expect(r, isNotNull);
|
|
expect(r!.isIgnore, isFalse);
|
|
expect(r.accountId, 'acc-1');
|
|
expect(r.categoryId, isNull);
|
|
expect(r.merchantCanonical, isNull);
|
|
expect(r.matchCount, 2);
|
|
});
|
|
|
|
test('ignore → is_ignore=1, disabled сохраняется', () async {
|
|
final r = await db.parseRulesDao.findById('r-ignore');
|
|
expect(r, isNotNull);
|
|
expect(r!.isIgnore, isTrue);
|
|
expect(r.pattern, 'заказ \\d+');
|
|
expect(r.matchMode, MatchMode.regex);
|
|
expect(r.enabled, isFalse, reason: 'enabled=0 переносится как есть');
|
|
});
|
|
}
|