Files
portal/app/tests/Unit/Autopodbor/Extract/DirectoryParserTest.php
T
Дмитрий df83fa836c feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00

286 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use App\Services\Autopodbor\Agent\Extract\DirectoryParser;
it('берёт ссылки на карточки филиалов из списка 2ГИС', function () {
$html = '<a href="/krasnoyarsk/firm/111">Ф1</a><a href="/krasnoyarsk/firm/222">Ф2</a>'
.'<a href="/krasnoyarsk/firm/111">дубль</a><a href="/about">не филиал</a>';
$links = (new DirectoryParser)->parseBranchList($html);
expect($links)->toBe(['/krasnoyarsk/firm/111', '/krasnoyarsk/firm/222']);
});
it('берёт ссылки на филиалы, даже когда они в данных страницы, а не в href', function () {
// 2ГИС иногда рендерит карточки без чистого href — ссылка лежит в JSON-состоянии страницы
$html = '{"items":[{"link":"/krasnoyarsk/firm/111"},{"link":"/krasnoyarsk/firm/222"}]}'
.'<a href="/krasnoyarsk/firm/111">дубль</a>';
$links = (new DirectoryParser)->parseBranchList($html);
expect($links)->toBe(['/krasnoyarsk/firm/111', '/krasnoyarsk/firm/222']);
});
it('из карточки 2ГИС берёт телефон из tel: и адрес из заголовка', function () {
$html = '<html><head><title>КрасЛомбард, улица Ладо Кецховели, 30, Красноярск — 2ГИС</title></head>'
.'<body><h1>КрасЛомбард</h1><a href="tel:+73912920000">+7 (391) 2...</a></body></html>';
$cards = (new DirectoryParser)->parseFirmCard($html, 'https://2gis.ru/krasnoyarsk/firm/111', '2ГИС');
expect($cards)->toHaveCount(1);
expect($cards[0]->number)->toBe('+73912920000');
expect($cards[0]->office)->toBe('улица Ладо Кецховели, 30');
expect($cards[0]->url)->toBe('https://2gis.ru/krasnoyarsk/firm/111');
expect($cards[0]->source)->toBe('2ГИС');
});
it('схлопывает неразрывные/двойные пробелы в адресе из заголовка', function () {
$html = "<title>КрасЛомбард,\u{00A0}улица Весны,\u{00A0}7а, Красноярск — 2ГИС</title>"
.'<a href="tel:+73912550000">x</a>';
$cards = (new DirectoryParser)->parseFirmCard($html, 'u', '2ГИС');
expect($cards[0]->office)->toBe('улица Весны, 7а');
});
// ── Яндекс.Карты ────────────────────────────────────────────────────────────
it('parseYandexCard: извлекает телефон из div orgpage-phones-view и сайт из business-urls-link', function () {
$html = <<<'HTML'
<title>Автодом24, микрофинансовая организация, Красноярск, Караульная ул., 88 — Яндекс Карты</title>
<div class="orgpage-phones-view__phone-number" dir="ltr">+7 (913) 539-65-46</div>
<div class="business-urls-view__url">
<a itemprop="url" class="business-urls-view__link" href="https://automoney-krsk.ru/" target="_blank" rel="nofollow">
<span class="business-urls-view__text">automoney-krsk.ru</span>
</a>
</div>
HTML;
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/avtodom24/8905494012/');
expect($cards)->not->toBeEmpty();
// Номер хранится в сыром виде «+7 (913) 539-65-46», проверяем по цифрам
$digitsAll = implode('', array_map(fn ($c) => preg_replace('/\D/', '', $c->number), $cards));
expect($digitsAll)->toContain('9135396546');
expect($cards[0]->siteUrl)->toContain('automoney-krsk.ru');
expect($cards[0]->source)->toBe('Яндекс.Карты');
});
it('parseYandexCard: дедуп телефонов — wa.me не дублирует основной номер', function () {
$html = <<<'HTML'
<title>Автодом24, микрофинансовая организация, Красноярск, Красная ул., 1 — Яндекс Карты</title>
<div class="orgpage-phones-view__phone-number" dir="ltr">+7 (913) 539-65-46</div>
<a href="https://wa.me/79135396546" role="button" itemprop="sameAs"></a>
<div class="business-urls-view__url">
<a class="business-urls-view__link" href="https://automoney-krsk.ru/"></a>
</div>
HTML;
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/avtodom24/1/');
// wa.me не должен дублировать тот же номер
expect($cards)->toHaveCount(1);
expect($cards[0]->siteUrl)->toContain('automoney-krsk.ru');
});
it('parseYandexCard: если телефонов нет но сайт есть — одна карточка с пустым number', function () {
$html = <<<'HTML'
<title>Тест, Красноярск — Яндекс Карты</title>
<div class="business-urls-view__url">
<a class="business-urls-view__link" href="https://example.com/"></a>
</div>
HTML;
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/test/1/');
expect($cards)->toHaveCount(1);
expect($cards[0]->number)->toBe('');
expect($cards[0]->siteUrl)->toContain('example.com');
expect($cards[0]->source)->toBe('Яндекс.Карты');
});
it('parseYandexCard: нет ни телефона, ни сайта — возвращает пустой массив', function () {
$html = '<title>Тест — Яндекс Карты</title><div>нет контактов</div>';
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/test/2/');
expect($cards)->toBe([]);
});
it('parseYandexCard: адрес офиса берётся из заголовка страницы Яндекса', function () {
$html = <<<'HTML'
<title>Автодом24, микрофинансовая организация, Красноярск, Караульная ул., 88 — Яндекс Карты</title>
<div class="orgpage-phones-view__phone-number" dir="ltr">+7 (913) 539-65-46</div>
HTML;
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/avtodom24/1/');
expect($cards)->not->toBeEmpty();
// officeFromTitle должен отрезать «Автодом24» и «Яндекс Карты», оставить «микрофинансовая организация, Красноярск, Караульная ул., 88»
// Точнее: первый кусок = имя, последний = город; середина = адрес
expect($cards[0]->office)->not->toBeNull();
});
// ── 2ГИС «вскрыть всё»: сайт + все телефоны + описание из карточки ───────────
it('parse2gisCard: сайт берёт из поля url объекта-контакта website, НЕ из value-редиректа', function () {
// Контакты 2ГИС — JSON-массив объектов. У сайта настоящий адрес в "url",
// а в "value" — редирект link.2gis.ru (его брать нельзя).
$html = '<title>AutoMoney, Красноярск, Ленина, 1 — 2ГИС</title>'
.'{"url":"http://automoney-krsk.ru","text":"automoney-krsk.ru","code":"70000001093406076","type":"website","value":"http://link.2gis.ru/redirect/xxx"}'
.'{"contact_text":"+79135396546","type":"phone","value":"+79135396546"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/111');
expect($cards)->not->toBeEmpty();
expect($cards[0]->siteUrl)->toContain('automoney-krsk.ru');
expect($cards[0]->siteUrl)->not->toContain('link.2gis.ru');
expect($cards[0]->source)->toBe('2ГИС');
});
it('parse2gisCard: берёт ВСЕ телефоны из value объектов phone, без дублей', function () {
$html = '<title>Тест, Красноярск — 2ГИС</title>'
.'{"contact_text":"+7 391 279-33-79","type":"phone","value":"+73912793379"}'
.'{"contact_text":"+7 391 296-01-11","type":"phone","value":"+73912960111"}'
.'{"contact_text":"+7 391 240-00-68","type":"phone","value":"+73912400068"}'
.'{"type":"phone","value":"+73912793379"}'; // дубль первого
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/222');
$digits = array_map(fn ($c) => preg_replace('/\D/', '', $c->number), $cards);
expect($digits)->toContain('73912793379');
expect($digits)->toContain('73912960111');
expect($digits)->toContain('73912400068');
expect(array_unique($digits))->toHaveCount(3);
});
it('parse2gisCard: достаёт описание «чем занимается» из og:description', function () {
$html = '<title>AutoMoney, Красноярск — 2ГИС</title>'
.'<meta property="og:description" content="Займы под залог ПТС/авто от 2,5% в месяц за 15 минут">'
.'{"contact_text":"+7 913 539-65-46","type":"phone","value":"+79135396546"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/333');
expect($cards)->not->toBeEmpty();
expect($cards[0]->description)->toContain('Займы под залог');
});
it('parse2gisCard: НЕ берёт служебную заглушку 2ГИС в описание (оценка/фото/отзывы)', function () {
$html = '<title>AutoMoney, Красноярск — 2ГИС</title>'
.'<meta property="og:description" content="Оценка 5, 11 фото, 93 отзыва. Вся информация об этом месте есть в 2ГИС">'
.'{"contact_text":"+7 913 539-65-46","type":"phone","value":"+79135396546"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/xx');
expect($cards)->not->toBeEmpty();
expect($cards[0]->description)->toBeNull(); // заглушку не пишем
});
it('parse2gisCard: рубрика из заголовка идёт в описание, когда своего описания нет', function () {
// og-описания нет → «чем занимается» берём из рубрики (2-й кусок заголовка): «Имя, РУБРИКА, адрес…, Город».
$html = '<title>Ломбарджини, ломбард, Взлётная улица, 6а, Красноярск — 2ГИС</title>'
.'{"url":"http://lombardzhini.ru","text":"x","code":"1","type":"website"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/1');
expect($cards[0]->description)->toBe('ломбард');
});
it('parse2gisCard: 2-й кусок-адрес НЕ считается рубрикой (описание пустое)', function () {
// «КрасЛомбард, улица Ладо Кецховели, 30, Красноярск» — 2-й кусок это АДРЕС, не рубрика → описания нет.
$html = '<title>КрасЛомбард, улица Ладо Кецховели, 30, Красноярск — 2ГИС</title>'
.'{"url":"http://k.ru","text":"x","code":"1","type":"website"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/2');
expect($cards[0]->description)->toBeNull();
});
it('parseYandexCard: рубрика из заголовка идёт в описание', function () {
$html = '<title>Твои Займы, микрофинансовая организация, Ястынская ул., 19А, Красноярск — Яндекс Карты</title>'
.'<a class="business-urls-view__link" href="https://tvoizaymy.ru/"></a>';
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/tvoi_zaymy/1/');
expect($cards[0]->description)->toBe('микрофинансовая организация');
});
it('parse2gisCard: мусор-домены из <script src> не считаются сайтом', function () {
// Счётчики/трекеры (top100, mc.yandex) сидят в <script src>, это НЕ контакт-сайт.
$html = '<title>Тест, Красноярск — 2ГИС</title>'
.'<script src="https://st.top100.ru/top100/top100.js"></script>'
.'<script src="https://mc.yandex.ru/metrika/tag.js"></script>'
.'{"url":"http://realsite.рф","text":"realsite.рф","code":"1","type":"website","value":"http://link.2gis.ru/y"}'
.'{"contact_text":"+7 391 200-00-00","type":"phone","value":"+73912000000"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/444');
expect($cards[0]->siteUrl)->toContain('realsite');
expect($cards[0]->siteUrl)->not->toContain('top100');
expect($cards[0]->siteUrl)->not->toContain('mc.yandex');
});
it('parse2gisCard: телефонов нет, но сайт есть — одна карточка с пустым number', function () {
$html = '<title>Тест, Красноярск — 2ГИС</title>'
.'{"url":"http://onlysite.ru","text":"onlysite.ru","code":"1","type":"website","value":"http://link.2gis.ru/z"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/555');
expect($cards)->toHaveCount(1);
expect($cards[0]->number)->toBe('');
expect($cards[0]->siteUrl)->toContain('onlysite.ru');
});
it('parse2gisCard: нет ни телефона, ни сайта — пустой массив', function () {
$html = '<title>Тест — 2ГИС</title><div>нет контактов</div>';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/666');
expect($cards)->toBe([]);
});
it('parseYandexCard: достаёт ВСЕ три телефона из скрытого JSON, без дублей', function () {
// Яндекс показывает только первый телефон в видимом блоке,
// остальные прячет за раскрывашкой — но все три лежат в JSON на той же странице.
$html = <<<'HTML'
<title>Автокредит24, Красноярск, Взлётная ул., 5 — Яндекс Карты</title>
<script>
window.__REDUX_STATE__={"phones":[{"number":"+7 (391) 279-33-79","type":"phone","value":"+73912793379"},{"number":"+7 (391) 296-01-11","type":"phone","value":"+73912960111"},{"number":"+7 (391) 240-00-68","type":"phone","value":"+73912400068"}]};
</script>
<div class="orgpage-phones-view__phone-number" dir="ltr">+7 (391) 279-33-79</div>
<div class="business-urls-view__url">
<a class="business-urls-view__link" href="https://autocredit24.ru/"></a>
</div>
HTML;
$cards = (new DirectoryParser)->parseYandexCard($html, 'https://yandex.ru/maps/org/autocredit24/1/');
// Должны получить ровно 3 карточки (по одной на телефон)
expect($cards)->toHaveCount(3);
// Проверяем по цифрам (без +, скобок и т.п.)
$allDigits = array_map(fn ($c) => preg_replace('/\D/', '', $c->number), $cards);
expect($allDigits)->toContain('73912793379');
expect($allDigits)->toContain('73912960111');
expect($allDigits)->toContain('73912400068');
// Нет дублей
expect(array_unique($allDigits))->toHaveCount(3);
// Сайт присутствует у каждой карточки
foreach ($cards as $card) {
expect($card->siteUrl)->toContain('autocredit24.ru');
}
});
it('parse2gisCard: НЕ берёт рекламный телефон-кнопку (caption/platforms), только настоящий контакт', function () {
// Живой случай: на карточке «Ломбард Бодрый» 2ГИС показал рекламный блок «Связаться с
// менеджером» — это кнопка-действие ЧУЖОЙ фирмы (Ломбард Айклауд). У неё caption/platforms и
// НЕТ code/print_text. Её номер брать нельзя: иначе чужой телефон попадает в контакты Бодрого
// и на «Предложениях» даёт ложное «объединить?».
$html = '<title>Ломбард Бодрый, ломбард, Красноярск — 2ГИС</title>'
// настоящий контакт фирмы: с code (id) и print_text
.'{"text":"+7 (391) 296-78-88","code":"70000001085640830","print_text":"+7 (391) 296-78-88","type":"phone","value":"+73912967888"}'
// рекламная кнопка чужой фирмы: caption + platforms, без code/print_text
.'{"caption":"Связаться с менеджером","name":"Связаться с менеджером","platforms":["ios","android","online"],"type":"phone","value":"+79237575575"}';
$cards = (new DirectoryParser)->parse2gisCard($html, 'https://2gis.ru/krasnoyarsk/firm/70000001085640828');
$digits = array_map(fn ($c) => preg_replace('/\D/', '', $c->number), $cards);
expect($digits)->toContain('73912967888'); // настоящий телефон Бодрого — берём
expect($digits)->not->toContain('79237575575'); // рекламный номер Айклауда — НЕ берём
});