156 lines
4.7 KiB
Dart
156 lines
4.7 KiB
Dart
import '../../domain/entities/parse_rule.dart';
|
|
import '../../domain/repositories/account_bindings_repository.dart';
|
|
import 'rule_lookup.dart';
|
|
|
|
/// Откуда взят разрешённый счёт (для диагностики / подсветки).
|
|
enum AccountSource {
|
|
bindingCard,
|
|
bindingPhone,
|
|
senderRule,
|
|
singleBinding,
|
|
appDefault,
|
|
ambiguous,
|
|
globalDefault,
|
|
none,
|
|
}
|
|
|
|
/// Результат разрешения счёта из уведомления (шаг 3 pipeline, §B).
|
|
class AccountResolution {
|
|
const AccountResolution({
|
|
required this.accountId,
|
|
required this.score,
|
|
required this.trusted,
|
|
required this.source,
|
|
this.bindingId,
|
|
});
|
|
|
|
/// Разрешённый счёт (null, если ничего не нашлось).
|
|
final String? accountId;
|
|
|
|
/// Confidence по счёту 0..100 (§8.2) — для подсветки «?», НЕ для gate.
|
|
final int score;
|
|
|
|
/// Можно ли доверять счёту для авто-применения. Именно это поле (а не score)
|
|
/// решает судьбу в gate: неоднозначность (#5) → false → Inbox; осознанные
|
|
/// дефолты (#4, #6) → true → авто-применение разрешено.
|
|
final bool trusted;
|
|
|
|
final AccountSource source;
|
|
|
|
/// id сработавшей привязки — для инкремента matchCount.
|
|
final String? bindingId;
|
|
}
|
|
|
|
/// Разрешает `accountId` по цепочке источников (§B): привязки карта/телефон,
|
|
/// `senderToAccount`-правила, per-app default и глобальный дефолт счёта.
|
|
class AccountResolver {
|
|
const AccountResolver(this._bindings);
|
|
|
|
final AccountBindingsRepository _bindings;
|
|
|
|
Future<AccountResolution> resolve({
|
|
required String userId,
|
|
required String packageName,
|
|
required String body,
|
|
String? cardLast4,
|
|
String? phone,
|
|
String? merchantRaw,
|
|
List<ParseRule> senderRules = const [],
|
|
String? globalDefaultAccountId,
|
|
}) async {
|
|
// #1 — binding по packageName + cardLast4.
|
|
if (cardLast4 != null) {
|
|
final b =
|
|
await _bindings.findByPackageAndCard(userId, packageName, cardLast4);
|
|
if (b != null) {
|
|
return AccountResolution(
|
|
accountId: b.accountId,
|
|
score: 100,
|
|
trusted: true,
|
|
source: AccountSource.bindingCard,
|
|
bindingId: b.id,
|
|
);
|
|
}
|
|
}
|
|
|
|
// #1 — binding по телефону (СБП).
|
|
if (phone != null) {
|
|
final b = await _bindings.findByPhone(userId, phone);
|
|
if (b != null) {
|
|
return AccountResolution(
|
|
accountId: b.accountId,
|
|
score: 100,
|
|
trusted: true,
|
|
source: AccountSource.bindingPhone,
|
|
bindingId: b.id,
|
|
);
|
|
}
|
|
}
|
|
|
|
// #2 — senderToAccount-правило (матч по телу).
|
|
final senderRule =
|
|
findSenderRule(senderRules, body: body, merchantRaw: merchantRaw);
|
|
if (senderRule?.accountId != null) {
|
|
return AccountResolution(
|
|
accountId: senderRule!.accountId,
|
|
score: 90,
|
|
trusted: true,
|
|
source: AccountSource.senderRule,
|
|
);
|
|
}
|
|
|
|
final byPkg = await _bindings.findByPackageName(userId, packageName);
|
|
|
|
// #3 — единственный binding по packageName.
|
|
if (byPkg.length == 1) {
|
|
return AccountResolution(
|
|
accountId: byPkg.first.accountId,
|
|
score: 75,
|
|
trusted: true,
|
|
source: AccountSource.singleBinding,
|
|
bindingId: byPkg.first.id,
|
|
);
|
|
}
|
|
|
|
if (byPkg.length > 1) {
|
|
// #4 — per-app default binding (card неизвестна).
|
|
final def = await _bindings.findDefaultByPackageName(userId, packageName);
|
|
if (def != null) {
|
|
return AccountResolution(
|
|
accountId: def.accountId,
|
|
score: 70,
|
|
trusted: true,
|
|
source: AccountSource.appDefault,
|
|
bindingId: def.id,
|
|
);
|
|
}
|
|
// #5 — несколько bindings без default-флага → «первый», но не доверяем.
|
|
return AccountResolution(
|
|
accountId: byPkg.first.accountId,
|
|
score: 45,
|
|
trusted: false,
|
|
source: AccountSource.ambiguous,
|
|
bindingId: byPkg.first.id,
|
|
);
|
|
}
|
|
|
|
// #6 — глобальный Account.isDefault.
|
|
if (globalDefaultAccountId != null) {
|
|
return AccountResolution(
|
|
accountId: globalDefaultAccountId,
|
|
score: 40,
|
|
trusted: true,
|
|
source: AccountSource.globalDefault,
|
|
);
|
|
}
|
|
|
|
// #7 — ничего.
|
|
return const AccountResolution(
|
|
accountId: null,
|
|
score: 15,
|
|
trusted: false,
|
|
source: AccountSource.none,
|
|
);
|
|
}
|
|
}
|