Files
portal/app/tests/Feature/Autopodbor/AutopodborCompetitorMergerTest.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

115 lines
5.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AutopodborCompetitor;
use App\Models\AutopodborRun;
use App\Models\AutopodborSource;
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Autopodbor\AutopodborCompetitorMerger;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
function mergerTenant(): Tenant
{
$t = Tenant::factory()->create(['balance_rub' => '100000.00']);
DB::statement('SET app.current_tenant_id = '.$t->id);
return $t;
}
function mkComp(Tenant $t, string $name, ?string $site, array $dirs = []): AutopodborCompetitor
{
$run = AutopodborRun::create(['tenant_id' => $t->id, 'kind' => 'search', 'status' => 'done', 'region_code' => 16, 'params' => []]);
return AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => $name,
'dedup_key' => 'c:'.md5($name), 'site_url' => $site, 'directory_urls' => $dirs,
]);
}
function mkSrc(Tenant $t, AutopodborCompetitor $c, string $type, string $ident, string $dedupKey, ?int $projectId = null, string $box = 'field'): AutopodborSource
{
$run = AutopodborRun::create(['tenant_id' => $t->id, 'kind' => 'study', 'status' => 'done', 'region_code' => 16, 'competitor_id' => $c->id, 'params' => []]);
return AutopodborSource::create([
'tenant_id' => $t->id, 'competitor_id' => $c->id, 'study_run_id' => $run->id,
'signal_type' => $type, 'identifier' => $ident, 'dedup_key' => $dedupKey,
'box' => $box, 'confirmations' => 1, 'created_project_id' => $projectId,
]);
}
it('переносит источники проигравшего на выжившего, дубль-конкурента удаляет, справочники объединяет', function () {
$t = mergerTenant();
$a = mkComp($t, 'AutoMoney', 'automoney-krsk.ru', ['https://2gis.ru/krasnoyarsk/firm/1']);
$b = mkComp($t, 'Автодом24', 'automoney-krsk.ru', ['https://yandex.ru/maps/org/x/2']);
mkSrc($t, $a, 'site', 'automoney-krsk.ru', 'site:automoney-krsk.ru');
mkSrc($t, $b, 'call', '79135396546', 'call:79135396546');
$survivorId = app(AutopodborCompetitorMerger::class)->merge($t->id, [$a->id, $b->id]);
expect(AutopodborSource::where('competitor_id', $survivorId)->count())->toBe(2);
$loserId = $survivorId === $a->id ? $b->id : $a->id;
expect(AutopodborCompetitor::find($loserId))->toBeNull();
$s = AutopodborCompetitor::find($survivorId);
expect($s->directory_urls)->toContain('https://2gis.ru/krasnoyarsk/firm/1')->toContain('https://yandex.ru/maps/org/x/2');
});
it('источник с проектом не теряется — переносится, created_project_id сохранён, конкурент с проектом выживает', function () {
$t = mergerTenant();
$a = mkComp($t, 'A', 'a.ru');
$b = mkComp($t, 'B', 'a.ru');
$project = Project::factory()->create(['tenant_id' => $t->id, 'is_active' => true, 'preflight_blocked_at' => null]);
mkSrc($t, $b, 'call', '79001112233', 'call:79001112233', $project->id);
$survivorId = app(AutopodborCompetitorMerger::class)->merge($t->id, [$a->id, $b->id]);
expect($survivorId)->toBe($b->id); // у B источник с проектом → он выживает
$src = AutopodborSource::where('created_project_id', $project->id)->first();
expect($src)->not->toBeNull();
expect($src->competitor_id)->toBe($survivorId);
});
it('коллизия ключа: у выжившего источник с проектом, дубль без проекта отброшен, проект цел', function () {
$t = mergerTenant();
$project = Project::factory()->create(['tenant_id' => $t->id, 'is_active' => true, 'preflight_blocked_at' => null]);
$a = mkComp($t, 'A', 'a.ru');
mkSrc($t, $a, 'call', '79001112233', 'call:79001112233', $project->id);
$b = mkComp($t, 'B', 'a.ru');
mkSrc($t, $b, 'call', '79001112233', 'call:79001112233'); // тот же ключ, без проекта
$survivorId = app(AutopodborCompetitorMerger::class)->merge($t->id, [$a->id, $b->id]);
expect($survivorId)->toBe($a->id);
expect(AutopodborSource::where('competitor_id', $survivorId)->where('dedup_key', 'call:79001112233')->count())->toBe(1);
$src = AutopodborSource::where('competitor_id', $survivorId)->where('dedup_key', 'call:79001112233')->first();
expect($src->created_project_id)->toBe($project->id);
});
it('три конкурента в группе → один выживший, два удалены, все источники у выжившего', function () {
$t = mergerTenant();
$a = mkComp($t, 'A', 'x.ru');
$b = mkComp($t, 'B', 'x.ru');
$c = mkComp($t, 'C', 'x.ru');
mkSrc($t, $a, 'site', 'x.ru', 'site:x.ru');
mkSrc($t, $b, 'call', '79001112233', 'call:79001112233');
mkSrc($t, $c, 'call', '79004445566', 'call:79004445566');
$survivorId = app(AutopodborCompetitorMerger::class)->merge($t->id, [$a->id, $b->id, $c->id]);
expect(AutopodborCompetitor::whereIn('id', [$a->id, $b->id, $c->id])->count())->toBe(1);
expect(AutopodborSource::where('competitor_id', $survivorId)->count())->toBe(3);
});
it('меньше двух конкурентов тенанта → исключение (ничего не сливаем)', function () {
$t = mergerTenant();
$a = mkComp($t, 'A', 'a.ru');
expect(fn () => app(AutopodborCompetitorMerger::class)->merge($t->id, [$a->id]))
->toThrow(InvalidArgumentException::class);
});