df83fa836c
Шаг 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>
197 lines
9.6 KiB
PHP
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);
|
|
});
|