Files
portal/app/tests/Feature/Autopodbor/AutopodborDuplicateGroupsFalsePositiveBugTest.php
T

198 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/**
* Расследование жалобы 13.07.2026: «Найти и объединить дубли» в «Конкурентном поле»
* склеила 5 РАЗНЫХ конкурентов демо-данных (OmegaDemoFieldSeeder) в одну группу дублей,
* и после «Объединить» 4 разных конкурента слились в одного.
*
* ТОЧНАЯ ПРИЧИНА (файл:строка): app/Services/Autopodbor/CompetitorIdentity.php,
* метод directoryKey() (до фикса) — регекс `#/firm/(\d+)#` и `#/maps/org/([a-z0-9_-]+)#i`
* извлекал ЛЮБОЙ id из ссылки справочника, включая литеральный плейсхолдер «0». Демо-сид
* OmegaDemoFieldSeeder.php (строки 68-69) ставит ОДИН И ТОТ ЖЕ плейсхолдер-адрес
* «https://2gis.ru/krasnoyarsk/firm/0» и «https://yandex.ru/maps/org/0» ВСЕМ пяти РАЗНЫМ
* фирмам (карточка справочника ещё «не вскрыта» — настоящий id неизвестен). Ключ «d:2gis:0»
* / «d:ya:0» получался ОДИНАКОВЫМ у 4 из 5 конкурентов → union-find в
* AutopodborDedup::duplicateGroups() (строки 183-251) считал их одной группой дублей —
* классическая транзитивная склейка (одна ложная связь тянет всех, у кого есть эта связь).
*
* ФИКС: CompetitorIdentity::directoryKey() теперь трактует id «0» как ПУСТОЙ/незначащий —
* настоящие id 2ГИС/Яндекса «0» не бывают, это только технический плейсхолдер «id не вскрыт».
* Тесты ниже используют РОВНО те же данные, что и database/seeders/OmegaDemoFieldSeeder.php
* (5 конкурентов поля «Омега», реальные РАЗНЫЕ сайты kraslombard24.ru / goldautoinvest.ru /
* fineo24.ru / cashmotor.ru / lockobank.ru).
*/
use App\Models\AutopodborCompetitor;
use App\Models\AutopodborRun;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
function fpBugSetup(): array
{
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
$run = AutopodborRun::create(['tenant_id' => $tenant->id, 'kind' => 'search', 'status' => 'done', 'region_code' => 24, 'params' => []]);
return [$tenant, $user, $run];
}
it('ФИКС ПОДТВЕРЖДЁН: демо-данные Омеги (5 РАЗНЫХ конкурентов, разные сайты, общий плейсхолдер firm/0) больше НЕ склеиваются в дубли', function () {
// Точная копия поля из OmegaDemoFieldSeeder::run() — 5 РЕАЛЬНО разных фирм с разными доменами,
// у которых карточки справочника ещё «не вскрыты» (плейсхолдер id «0» вместо настоящего).
[$t, $u, $run] = fpBugSetup();
$gis = 'https://2gis.ru/krasnoyarsk/firm/0'; // плейсхолдер-id "0" — карточка есть, id не вскрыт
$ya = 'https://yandex.ru/maps/org/0'; // тот же плейсхолдер у нескольких фирм
$field = [
['КрасЛомбард', 'kraslombard24.ru', [$gis, $ya]],
['Голд Авто Инвест', 'goldautoinvest.ru', [$gis, $ya]],
['Финео', 'fineo24.ru', [$gis]],
['Cashmotor', 'cashmotor.ru', []],
['Локо-Банк', 'lockobank.ru', [$ya]],
];
foreach ($field as [$name, $site, $dirs]) {
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => $name, 'site_url' => $site, 'directory_urls' => $dirs,
'dedup_key' => 'site:'.$site, 'box' => 'field',
]);
}
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
// 5 РАЗНЫХ сайтов → 0 групп дублей вообще (ни одна пара из этих пяти не является реальным
// дублем — только плейсхолдер "0" в ссылке-заглушке совпадал, а это не признак совпадения).
$resp->assertJsonCount(0, 'groups');
});
it('ФИКС: у клиента нет ни одной группы для нажатия «Объединить» — реальные разные конкуренты остаются 5 отдельными карточками', function () {
// Тот же сетап — раньше здесь 4 из 5 конкурентов схлопывались одним «Объединить». Проверяем,
// что после фикса все 5 карточек живы и по отдельности.
[$t, $u, $run] = fpBugSetup();
$gis = 'https://2gis.ru/krasnoyarsk/firm/0';
$ya = 'https://yandex.ru/maps/org/0';
$field = [
['КрасЛомбард', 'kraslombard24.ru', [$gis, $ya]],
['Голд Авто Инвест', 'goldautoinvest.ru', [$gis, $ya]],
['Финео', 'fineo24.ru', [$gis]],
['Cashmotor', 'cashmotor.ru', []],
['Локо-Банк', 'lockobank.ru', [$ya]],
];
foreach ($field as [$name, $site, $dirs]) {
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => $name, 'site_url' => $site, 'directory_urls' => $dirs,
'dedup_key' => 'site:'.$site, 'box' => 'field',
]);
}
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')
->assertOk()
->assertJsonCount(0, 'groups');
expect(AutopodborCompetitor::where('tenant_id', $t->id)->count())->toBe(5);
});
it('НАСТОЯЩИЙ дубль по справочнику (одинаковый НЕПУСТОЙ id 2ГИС) — по-прежнему находится и склеивается', function () {
// Защита от перегиба: фикс должен резать только плейсхолдер "0", а не ломать реальную
// склейку одной и той же карточки справочника (id 111 — настоящий, не плейсхолдер).
[$t, $u, $run] = fpBugSetup();
$a = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Яричъ Ломбард', 'site_url' => 'old-yarich.ru', 'directory_urls' => ['https://2gis.ru/krasnoyarsk/firm/111'],
'dedup_key' => 'site:old-yarich.ru', 'box' => 'field',
]);
$b = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Яричъ (новый сайт)', 'site_url' => 'new-yarich.ru', 'directory_urls' => ['https://2gis.ru/krasnoyarsk/firm/111'],
'dedup_key' => 'site:new-yarich.ru', 'box' => 'field',
]);
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
$resp->assertJsonCount(1, 'groups');
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('справочник');
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
});
it('НАСТОЯЩИЙ дубль по САЙТУ (одинаковый непустой site_url) — находится, причина «совпал сайт»', function () {
[$t, $u, $run] = fpBugSetup();
$a = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Окна Комфорт (карточка 1)', 'site_url' => 'okna-komfort.ru', 'directory_urls' => [],
'dedup_key' => 'site:okna-komfort.ru:1', 'box' => 'field',
]);
$b = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Окна Комфорт (карточка 2)', 'site_url' => 'okna-komfort.ru', 'directory_urls' => [],
'dedup_key' => 'site:okna-komfort.ru:2', 'box' => 'field',
]);
// третий, с другим сайтом — в группу попасть не должен
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Совсем другая фирма', 'site_url' => 'sovsem-drugaya.ru', 'directory_urls' => [],
'dedup_key' => 'site:sovsem-drugaya.ru', 'box' => 'field',
]);
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
$resp->assertJsonCount(1, 'groups');
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('сайт');
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
});
it('ОПРОВЕРЖЕНИЕ гипотезы «пустой сайт»: 5 конкурентов БЕЗ сайта (site_url=null) — CompetitorIdentity корректно НЕ склеивает их по «пустому» ключу', function () {
[$t, $u, $run] = fpBugSetup();
$names = ['Ромашка', 'Восток', 'Северный Ветер', 'Прогресс', 'Гарант'];
foreach ($names as $i => $name) {
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => $name, 'site_url' => null, 'directory_urls' => [],
'dedup_key' => 'noSite:'.$i, 'box' => 'field',
]);
}
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
// Без сайта, без справочников, без телефонов, без общего адреса — опознавалок нет вовсе,
// группировать не по чему. Ожидание: 0 групп.
$resp->assertJsonCount(0, 'groups');
});
it('ОПРОВЕРЖЕНИЕ гипотезы «подстрока вместо равенства»: близкие, но РАЗНЫЕ домены не считаются дублями', function () {
[$t, $u, $run] = fpBugSetup();
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Окна Комфорт', 'site_url' => 'okna-komfort.ru', 'directory_urls' => [],
'dedup_key' => 'site:okna-komfort.ru', 'box' => 'field',
]);
AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => 'Окна Комфорт Плюс', 'site_url' => 'okna-komfort-plus.ru', 'directory_urls' => [],
'dedup_key' => 'site:okna-komfort-plus.ru', 'box' => 'field',
]);
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')->assertOk();
$resp->assertJsonCount(0, 'groups');
});