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

197 lines
9.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AutopodborCompetitor;
use App\Models\AutopodborRun;
use App\Models\AutopodborSource;
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 dupSetup(): 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' => 'study', 'status' => 'done', 'region_code' => 16, 'params' => []]);
return [$tenant, $user, $run];
}
function dupComp(Tenant $t, AutopodborRun $run, string $name): AutopodborCompetitor
{
return AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => $name, 'dedup_key' => 'c:'.md5($name), 'box' => 'field',
]);
}
function dupCall(Tenant $t, AutopodborRun $run, AutopodborCompetitor $c, string $phone): void
{
AutopodborSource::create([
'tenant_id' => $t->id, 'competitor_id' => $c->id, 'study_run_id' => $run->id,
'signal_type' => 'call', 'identifier' => $phone, 'dedup_key' => 'call:'.$phone,
'box' => 'field', 'confirmations' => 1,
]);
}
it('GET field/duplicate-groups — группирует конкурентов с общим телефоном, отдаёт имена и причину', function () {
[$t, $u, $run] = dupSetup();
$a = dupComp($t, $run, 'AutoMoney');
$b = dupComp($t, $run, 'Автодом24');
$c = dupComp($t, $run, 'Другая'); // без общих контактов
dupCall($t, $run, $a, '79135396546');
dupCall($t, $run, $b, '79135396546');
dupCall($t, $run, $c, '79990001122');
$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());
$names = collect($resp->json('groups.0.competitors'))->pluck('name')->all();
expect($names)->toContain('AutoMoney')->toContain('Автодом24');
});
it('GET field/duplicate-groups?box=proposal — группирует ТОЛЬКО предложения, поле не мешает', function () {
[$t, $u, $run] = dupSetup();
// Два дубля в предложениях
$p1 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл A', 'dedup_key' => 'p:a', 'box' => 'proposal']);
$p2 = AutopodborCompetitor::create(['tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл B', 'dedup_key' => 'p:b', 'box' => 'proposal']);
dupCall($t, $run, $p1, '79135396546');
dupCall($t, $run, $p2, '79135396546');
// Дубль в поле — не должен попасть в выборку по box=proposal
$f1 = dupComp($t, $run, 'Поле A');
$f2 = dupComp($t, $run, 'Поле B');
dupCall($t, $run, $f1, '79990001122');
dupCall($t, $run, $f2, '79990001122');
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
$resp->assertJsonCount(1, 'groups');
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
expect($ids)->toBe(collect([$p1->id, $p2->id])->sort()->values()->all());
});
function dupProp(Tenant $t, AutopodborRun $run, string $name, array $phones): AutopodborCompetitor
{
return AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id,
'name' => $name, 'dedup_key' => 'p:'.md5($name), 'box' => 'proposal', 'phones' => $phones,
]);
}
it('предложения: общий телефон из сохранённого поля (БЕЗ источников) ловится, причина — тот номер', function () {
// Шаг 3: у предложений источников нет — телефоны берём из поля phones конкурента.
[$t, $u, $run] = dupSetup();
$a = dupProp($t, $run, 'AutoMoney', ['79135396546']);
$b = dupProp($t, $run, 'Автодом24', ['79135396546', '73912000000']);
dupProp($t, $run, 'Другая', ['79990001122']); // свой номер — не в группе
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
$resp->assertJsonCount(1, 'groups');
// причина = именно СВЯЗАВШИЙ номер, а не весь список телефонов
expect($resp->json('groups.0.reason'))->toContain('79135396546')
->and($resp->json('groups.0.reason'))->not->toContain('73912000000');
$ids = collect($resp->json('groups.0.competitors'))->pluck('id')->sort()->values()->all();
expect($ids)->toBe(collect([$a->id, $b->id])->sort()->values()->all());
});
it('группа несёт сайт и ссылки-справочники каждой фирмы — чтобы клиент мог проверить и решить', function () {
[$t, $u, $run] = dupSetup();
$a = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Сфера займа', 'dedup_key' => 'p:sf',
'box' => 'proposal', 'phones' => ['73912329282'],
'site_url' => 'sfera.ru', 'directory_urls' => ['https://2gis.ru/krasnoyarsk/firm/1'],
]);
$b = AutopodborCompetitor::create([
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Ломбард Экспресс', 'dedup_key' => 'p:le',
'box' => 'proposal', 'phones' => ['73912329282'],
'site_url' => null, 'directory_urls' => ['https://yandex.ru/maps/org/le/2'],
]);
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')->assertOk();
$comp = collect($resp->json('groups.0.competitors'))->keyBy('name');
expect($comp['Сфера займа']['site_url'])->toBe('sfera.ru')
->and($comp['Сфера займа']['directory_urls'])->toContain('https://2gis.ru/krasnoyarsk/firm/1')
->and($comp['Ломбард Экспресс']['site_url'])->toBeNull()
->and($comp['Ломбард Экспресс']['directory_urls'])->toContain('https://yandex.ru/maps/org/le/2');
});
it('предложения: рекламный номер у слишком многих (>4) НЕ группирует (порог людного номера)', function () {
[$t, $u, $run] = dupSetup();
for ($i = 1; $i <= 5; $i++) {
dupProp($t, $run, 'Фирма '.$i, ['79135396546']); // один рекламный номер у пятерых
}
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')
->assertOk()
->assertJsonCount(0, 'groups');
});
it('GET field/duplicate-groups — группирует по общему адресу источников (слабый сигнал)', function () {
[$t, $u, $run] = dupSetup();
$a = dupComp($t, $run, 'ГорЗайм');
$b = dupComp($t, $run, 'Центр Займов');
// Разные телефоны, но ОДИН адрес → должно предложить объединение по адресу.
foreach ([[$a, '79001112233'], [$b, '79004445566']] as [$comp, $phone]) {
AutopodborSource::create([
'tenant_id' => $t->id, 'competitor_id' => $comp->id, 'study_run_id' => $run->id,
'signal_type' => 'call', 'identifier' => $phone, 'dedup_key' => 'call:'.$phone,
'box' => 'field', 'confirmations' => 1, 'office' => 'Водопьянова 2',
]);
}
$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('GET field/duplicate-groups — дублей нет → пустой список групп', function () {
[$t, $u, $run] = dupSetup();
$a = dupComp($t, $run, 'A');
$b = dupComp($t, $run, 'B');
dupCall($t, $run, $a, '79135396546');
dupCall($t, $run, $b, '79990001122');
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups')
->assertOk()
->assertJsonCount(0, 'groups');
});
it('POST field/merge — сливает подтверждённую группу, возвращает выжившего, дубль исчезает', function () {
[$t, $u, $run] = dupSetup();
$a = dupComp($t, $run, 'A');
$b = dupComp($t, $run, 'B');
dupCall($t, $run, $a, '79135396546');
dupCall($t, $run, $b, '79135396546');
$resp = $this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
'competitor_ids' => [$a->id, $b->id],
])->assertOk();
$survivor = $resp->json('survivor_id');
expect(AutopodborCompetitor::whereIn('id', [$a->id, $b->id])->count())->toBe(1);
expect($survivor)->toBeIn([$a->id, $b->id]);
});
it('POST field/merge — меньше двух id → 422', function () {
[$t, $u, $run] = dupSetup();
$a = dupComp($t, $run, 'A');
$this->actingAs($u)->postJson('/api/autopodbor/field/merge', [
'competitor_ids' => [$a->id],
])->assertStatus(422);
});