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>
50 lines
1.9 KiB
PHP
50 lines
1.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\Phone\DaDataValidityLayer;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
beforeEach(function (): void {
|
|
config(['services.dadata.daily_cap_rub' => 10000]); // бюджет открыт
|
|
});
|
|
|
|
it('qc=2 (мусор) → номер невалиден (выкидываем)', function (): void {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 2]], 200)]);
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeFalse();
|
|
});
|
|
|
|
it('qc=7 (иностранец) → номер невалиден', function (): void {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 7]], 200)]);
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeFalse();
|
|
});
|
|
|
|
it('qc=0 (годный) → валиден', function (): void {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0]], 200)]);
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeTrue();
|
|
});
|
|
|
|
it('qc=1 (неоднозначный) → оставляем (не режем по сомнению)', function (): void {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 1]], 200)]);
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeTrue();
|
|
});
|
|
|
|
it('бюджет исчерпан → не тратим и не режем, ДаДату не дёргаем', function (): void {
|
|
config(['services.dadata.daily_cap_rub' => 0]); // касса закрыта
|
|
Http::fake();
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeTrue();
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('сбой ДаДаты (сеть) → не роняем и не режем', function (): void {
|
|
Http::fake(fn () => throw new ConnectionException('timeout'));
|
|
|
|
expect(app(DaDataValidityLayer::class)->isValid('79215555123'))->toBeTrue();
|
|
});
|