198 lines
11 KiB
PHP
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');
|
||
|
|
});
|