a028af4bc8
Экстрактор кода страницы ловил любые 11 цифр (7/8 + телефонная группировка) без проверки, что это настоящий российский номер — со страниц-агрегаторов пролезал мусор с несуществующими кодами (227/303/453/613/761) и подписывался «городской». PhoneValidityGate между CandidateBuilder и SourceAggregator: - слой 1 (бесплатно, всегда): реестр Россвязи (phone_ranges) — нет в реестре → выкидываем; - слой 2 (платно, за флагом autopodbor.phone_dadata_enabled, по умолчанию ВЫКЛ): DaDataValidityLayer перепроверяет прошедшие реестр номера, снимает qc 2/7, под дневным бюджетом + кэш вердикта; платим только за выживших (мусор отсекается ДО ДаДаты). - fail-open при пустом реестре / сбое БД (иначе внешний catch в SiteOpener съел бы все номера); - короткие/uncertain номера не трогаем (нельзя сверить). Закрыты ОБА пути шага 2: обычный (RealCompetitorAgent) и глубокий (SiteOpener). Миграций нет — реестр уже на проде (453k диапазонов). Тесты: PhoneValidityGateTest (7), DaDataValidityLayerTest (6), SiteOpenerValidityTest (1); весь набор автоподбора 519 зелёных, Pint + Larastan чисто. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
5.6 KiB
PHP
154 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\Extract\PhoneValidityGate;
|
|
use App\Services\Autopodbor\Agent\Extract\PhoneValidityLayer;
|
|
use App\Services\RossvyazPrefixLookup;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
uses(SharesSupplierPdo::class);
|
|
|
|
/**
|
|
* Засеваем один диапазон Россвязи → реестр становится непустым.
|
|
* По умолчанию: def_code=921, абоненты 5550000..5559999 (МегаФон, СПб).
|
|
*
|
|
* @param array<string, mixed> $overrides
|
|
*/
|
|
function seedGateRange(array $overrides = []): void
|
|
{
|
|
$importId = (int) DB::table('phone_ranges_imports')->insertGetId([
|
|
'source_url' => 'https://rossvyaz.gov.ru/test',
|
|
'checksum_sha256' => str_repeat('b', 64),
|
|
'status' => 'completed',
|
|
'imported_at' => now(),
|
|
]);
|
|
|
|
DB::table('phone_ranges')->insert(array_merge([
|
|
'def_code' => 921,
|
|
'from_num' => 5550000,
|
|
'to_num' => 5559999,
|
|
'operator' => 'МегаФон',
|
|
'region' => 'Санкт-Петербург',
|
|
'subject_code' => 83,
|
|
'imported_at' => now(),
|
|
'import_id' => $importId,
|
|
], $overrides));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array{number:string,kind:string,label:string,url:?string,office:?string,tracker:bool}
|
|
*/
|
|
function gateCandidate(string $number, array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'number' => $number,
|
|
'kind' => 'text',
|
|
'label' => 'в тексте сайта — проверьте',
|
|
'url' => 'https://review-group.ru/',
|
|
'office' => null,
|
|
'tracker' => false,
|
|
], $overrides);
|
|
}
|
|
|
|
it('выкидывает номер, чей код города отсутствует в реестре Россвязи', function (): void {
|
|
seedGateRange(); // реестр непустой (есть 921)
|
|
|
|
// 76132306130 — код «613», такого в реестре нет (мусор со страницы-агрегатора).
|
|
$out = app(PhoneValidityGate::class)->filter([gateCandidate('76132306130')]);
|
|
|
|
expect($out)->toBe([]);
|
|
});
|
|
|
|
it('оставляет номер, который есть в реестре Россвязи', function (): void {
|
|
seedGateRange(); // def_code=921, 5550000..5559999
|
|
|
|
$out = app(PhoneValidityGate::class)->filter([
|
|
gateCandidate('79215555123', ['kind' => 'code', 'label' => 'в коде сайта']),
|
|
]);
|
|
|
|
expect($out)->toHaveCount(1)
|
|
->and($out[0]['number'])->toBe('79215555123');
|
|
});
|
|
|
|
it('фильтрует только мусор, реальные соседи остаются', function (): void {
|
|
seedGateRange();
|
|
|
|
$out = app(PhoneValidityGate::class)->filter([
|
|
gateCandidate('76132306130'), // мусор (код 613)
|
|
gateCandidate('79215555123', ['kind' => 'code']), // реальный (921)
|
|
gateCandidate('72279347817'), // мусор (код 227 — вообще <300, запрещён реестром)
|
|
]);
|
|
|
|
expect($out)->toHaveCount(1)
|
|
->and($out[0]['number'])->toBe('79215555123');
|
|
});
|
|
|
|
it('при пустом реестре не режет ничего (fail-open)', function (): void {
|
|
// seedGateRange НЕ вызван → phone_ranges пуст (как на dev). Фильтр не должен
|
|
// выкосить всё из-за незагруженного реестра.
|
|
$out = app(PhoneValidityGate::class)->filter([
|
|
gateCandidate('76132306130'),
|
|
gateCandidate('79215555123'),
|
|
]);
|
|
|
|
expect($out)->toHaveCount(2);
|
|
});
|
|
|
|
it('никогда не трогает короткие номера «требует проверки» (их нельзя сверить)', function (): void {
|
|
seedGateRange();
|
|
|
|
// 2713333 — локальный 7-значный, код города не определён (kind=uncertain).
|
|
// Сверить с реестром нельзя → не теряем.
|
|
$out = app(PhoneValidityGate::class)->filter([
|
|
gateCandidate('2713333', ['kind' => 'uncertain']),
|
|
]);
|
|
|
|
expect($out)->toHaveCount(1)
|
|
->and($out[0]['number'])->toBe('2713333');
|
|
});
|
|
|
|
it('второй слой (ДаДата) снимает мусор, прошедший реестр', function (): void {
|
|
seedGateRange(); // 79215555123 есть в реестре
|
|
|
|
$dropAll = new class implements PhoneValidityLayer
|
|
{
|
|
public function isValid(string $number): bool
|
|
{
|
|
return false; // ДаДата сказала «мусор»
|
|
}
|
|
};
|
|
$gate = new PhoneValidityGate(new RossvyazPrefixLookup, $dropAll);
|
|
|
|
$out = $gate->filter([gateCandidate('79215555123', ['kind' => 'code'])]);
|
|
|
|
expect($out)->toBe([]);
|
|
});
|
|
|
|
it('второй слой НЕ вызывается для номеров вне реестра (мусор отсёк слой 1 бесплатно)', function (): void {
|
|
seedGateRange();
|
|
|
|
$spy = new class implements PhoneValidityLayer
|
|
{
|
|
public bool $called = false;
|
|
|
|
public function isValid(string $number): bool
|
|
{
|
|
$this->called = true;
|
|
|
|
return true;
|
|
}
|
|
};
|
|
$gate = new PhoneValidityGate(new RossvyazPrefixLookup, $spy);
|
|
|
|
// 76132306130 — код 613, вне реестра → слой 1 выкидывает бесплатно, ДаДата не дёргается.
|
|
$out = $gate->filter([gateCandidate('76132306130')]);
|
|
|
|
expect($out)->toBe([])
|
|
->and($spy->called)->toBeFalse();
|
|
});
|