Files
portal/app/tests/Unit/Autopodbor/Study/PhoneReverseExpanderTest.php
T
Дмитрий 2aace1ff8e fix(автоподбор): гейт «та же фирма» на разворот 2ГИС — режет иногородних чужаков
Баг AutoMoney: разворот по общему/партнёрскому/иногороднему номеру тащил чужие МФО
из Москвы/Питера (moneyman, finombudsman — фин.омбудсмен!, npmir, razvitie…) в источники
красноярского конкурента. crowdedCap ловил только массовые 8-800.

Гейт (100%-надёжный, проверен на реальных данных прода): карточку 2ГИС из разворота
выкидываем, ТОЛЬКО если город из URL карточки (2gis.ru/<city>/firm) ≠ город конкурента
И домен не наш бренд (с учётом поддоменов-филиалов). Свой город ИЛИ свой бренд — оставляем.
Яндекс НЕ трогаем (город в URL не виден, там же законные филиалы — риск срезать своих).

Проверка на проде: #16 AutoMoney/#64 Ваш Инвестор — все 2ГИС moscow/spb уходят;
#22 (автоломбард krasnoyarsk), #48 Кредит-финанс (Екб/Томск, Яндекс) — остаются.
11/11 тестов expander, вся ветка 220 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:50:38 +03:00

243 lines
11 KiB
PHP

<?php
use App\Services\Autopodbor\Agent\Fetch\PageFetcher;
use App\Services\Autopodbor\Agent\Study\PhoneReverseExpander;
/** Fake-загрузчик: карта url → html; всё остальное — пустая строка (как реальный xfetch без ключа). */
function fakeFetcher(array $map): PageFetcher
{
return new class($map) implements PageFetcher
{
/** @param array<string,string> $map */
public function __construct(private array $map) {}
public function html(string $url): string
{
return $this->map[$url] ?? '';
}
};
}
it('по одному личному номеру находит карточку проекта: её сайт и телефон', function () {
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm = 'https://2gis.ru/krasnoyarsk/firm/111';
$fetch = fakeFetcher([
$search => '<a href="/krasnoyarsk/firm/111">Звезда, жилой комплекс</a>',
$firm => '{"url":"https://zvezda-dsk.ru","text":"сайт","type":"website"}'
.'{"type":"phone","value":"+73912002288"}',
]);
$exp = new PhoneReverseExpander($fetch);
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
$domains = array_column($res->sites, 'domain');
$phones = array_column($res->phones, 'digits');
expect($domains)->toContain('zvezda-dsk.ru')
->and($phones)->toContain('73912002288');
});
it('общий номер (фирм больше порога) пропускается — находок нет', function () {
$search = 'https://2gis.ru/krasnoyarsk/search/78001234567';
$links = '';
foreach (range(201, 205) as $id) {
$links .= '<a href="/krasnoyarsk/firm/'.$id.'">Фирма '.$id.'</a>';
}
$fetch = fakeFetcher([$search => $links]);
$exp = new PhoneReverseExpander($fetch, crowdedCap: 4);
$res = $exp->expand('krasnoyarsk', ['78001234567'], []);
expect($res->sites)->toBe([])
->and($res->phones)->toBe([]);
});
it('пустой слаг города — разворот невозможен, пустой результат', function () {
$exp = new PhoneReverseExpander(fakeFetcher([]));
$res = $exp->expand('', ['73912273531'], []);
expect($res->sites)->toBe([])->and($res->phones)->toBe([]);
});
it('копает до талого при maxDepth>=2: телефон A → карточка с телефоном B → карточка B даёт сайт проекта', function () {
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
$searchB = 'https://2gis.ru/krasnoyarsk/search/73912002288';
$firm2 = 'https://2gis.ru/krasnoyarsk/firm/222';
$fetch = fakeFetcher([
$searchA => '<a href="/krasnoyarsk/firm/111">Управляющая компания</a>',
$firm1 => '{"type":"phone","value":"+73912002288"}',
$searchB => '<a href="/krasnoyarsk/firm/222">Аллея парк, жилой комплекс</a>',
$firm2 => '{"url":"https://alleya-park.ru","type":"website"}{"type":"phone","value":"+73912002288"}',
]);
$exp = new PhoneReverseExpander($fetch, maxDepth: 2);
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
expect(array_column($res->sites, 'domain'))->toContain('alleya-park.ru');
});
it('по умолчанию (глубина 1) НЕ уходит в цепочку — только фирмы с ЛИЧНЫМ номером, дрейф отсечён', function () {
// Те же данные, что и в тесте «до талого», но БЕЗ maxDepth (по умолчанию 1).
// firm/222 (Аллея парк) достижим только по номеру, найденному на firm/111 — цепочкой.
// При глубине 1 мы туда не идём: это и есть защита от дрейфа в чужие фирмы.
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
$searchB = 'https://2gis.ru/krasnoyarsk/search/73912002288';
$firm2 = 'https://2gis.ru/krasnoyarsk/firm/222';
$fetch = fakeFetcher([
$searchA => '<a href="/krasnoyarsk/firm/111">Управляющая компания</a>',
$firm1 => '{"type":"phone","value":"+73912002288"}',
$searchB => '<a href="/krasnoyarsk/firm/222">Аллея парк, жилой комплекс</a>',
$firm2 => '{"url":"https://alleya-park.ru","type":"website"}{"type":"phone","value":"+73912002288"}',
]);
$exp = new PhoneReverseExpander($fetch);
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
expect(array_column($res->sites, 'domain'))->not->toContain('alleya-park.ru');
});
it('одну и ту же карточку не открывает дважды (visited-set)', function () {
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
$searchB = 'https://2gis.ru/krasnoyarsk/search/73912002288';
$fetch = new class($searchA, $firm1, $searchB) implements PageFetcher
{
public int $opens = 0;
public function __construct(private string $sA, private string $f1, private string $sB) {}
public function html(string $url): string
{
if ($url === $this->sA || $url === $this->sB) {
return '<a href="/krasnoyarsk/firm/111">Фирма</a>';
}
if ($url === $this->f1) {
$this->opens++;
return '{"type":"phone","value":"+73912002288"}';
}
return '';
}
};
$exp = new PhoneReverseExpander($fetch);
$exp->expand('krasnoyarsk', ['73912273531'], []);
expect($fetch->opens)->toBe(1);
});
it('потолок карточек не превышается', function () {
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$fetch = new class($searchA) implements PageFetcher
{
public array $opens = [];
public function __construct(private string $sA) {}
public function html(string $url): string
{
if ($url === $this->sA) {
return '<a href="/krasnoyarsk/firm/111">A</a><a href="/krasnoyarsk/firm/222">B</a>';
}
$this->opens[] = $url;
return '{"type":"phone","value":"+7391200'.substr($url, -4).'"}';
}
};
$exp = new PhoneReverseExpander($fetch, maxFirms: 1);
$exp->expand('krasnoyarsk', ['73912273531'], []);
expect(count($fetch->opens))->toBe(1);
});
it('уже известный домен фирмы в находки не повторяет (не дублирует главную карточку)', function () {
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
$fetch = fakeFetcher([
$searchA => '<a href="/krasnoyarsk/firm/111">ДСК</a>',
$firm1 => '{"url":"https://gk-dsk.ru","type":"website"}',
]);
$exp = new PhoneReverseExpander($fetch);
$res = $exp->expand('krasnoyarsk', ['73912273531'], ['gk-dsk.ru']);
expect(array_column($res->sites, 'domain'))->not->toContain('gk-dsk.ru');
});
it('сайт без схемы в карточке — добавляем https://', function () {
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$firm = 'https://2gis.ru/krasnoyarsk/firm/111';
$fetch = fakeFetcher([
$search => '<a href="/krasnoyarsk/firm/111">Проект</a>',
$firm => '{"url":"zvezda-dsk.ru","type":"website"}',
]);
$exp = new PhoneReverseExpander($fetch);
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
$site = array_values(array_filter($res->sites, fn ($s) => $s['domain'] === 'zvezda-dsk.ru'))[0] ?? null;
expect($site)->not->toBeNull()
->and($site['site_url'])->toBe('https://zvezda-dsk.ru');
});
it('гейт «та же фирма»: чужой город + чужой домен выкидывает; свой город и свой бренд оставляет', function () {
// Баг AutoMoney: разворот по общему/партнёрскому номеру тащит чужие МФО из Москвы/Питера.
// Город карточки 2ГИС берём из ссылки (…/<city>/firm/…) — 100% надёжно.
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$own = 'https://2gis.ru/krasnoyarsk/firm/111'; // свой город
$foreign = 'https://2gis.ru/moscow/firm/222'; // чужой город + чужой домен → выкинуть
$branch = 'https://2gis.ru/ekaterinburg/firm/333'; // чужой город, НО наш бренд (поддомен) → оставить
$fetch = fakeFetcher([
$search => '<a href="/krasnoyarsk/firm/111">Наша</a>'
.'<a href="/moscow/firm/222">Чужая МФО</a>'
.'<a href="/ekaterinburg/firm/333">Наш филиал</a>',
$own => '{"url":"https://my-krsk.ru","type":"website"}{"type":"phone","value":"+73912002288"}',
$foreign => '{"url":"https://moneyman.ru","type":"website"}{"type":"phone","value":"+74991234567"}',
$branch => '{"url":"https://ekb.my-krsk.ru","type":"website"}{"type":"phone","value":"+73431112233"}',
]);
$exp = new PhoneReverseExpander($fetch);
$res = $exp->expand('krasnoyarsk', ['73912273531'], ['my-krsk.ru']);
$domains = array_column($res->sites, 'domain');
$phones = array_column($res->phones, 'digits');
// свой город — оставлен (сайт = seed-домен, дедуплен; проверяем по телефону)
expect($phones)->toContain('73912002288');
// чужой город + чужой домен — выкинут целиком (и сайт, и телефон)
expect($domains)->not->toContain('moneyman.ru')
->and($phones)->not->toContain('74991234567');
// чужой город, но наш бренд (поддомен my-krsk.ru) — оставлен
expect($domains)->toContain('ekb.my-krsk.ru')
->and($phones)->toContain('73431112233');
});
it('ровно порог фирм — НЕ общая линия, находки берём', function () {
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
$links = '';
foreach (range(301, 304) as $id) { // ровно 4 = crowdedCap → не общая линия
$links .= '<a href="/krasnoyarsk/firm/'.$id.'">Фирма '.$id.'</a>';
}
$map = [
$search => $links,
'https://2gis.ru/krasnoyarsk/firm/301' => '{"url":"https://proekt-301.ru","type":"website"}',
'https://2gis.ru/krasnoyarsk/firm/302' => '',
'https://2gis.ru/krasnoyarsk/firm/303' => '',
'https://2gis.ru/krasnoyarsk/firm/304' => '',
];
$exp = new PhoneReverseExpander(fakeFetcher($map), crowdedCap: 4);
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
expect(array_column($res->sites, 'domain'))->toContain('proekt-301.ru');
});