a28058779b
Кейс «Образцово»: у застройщика (СМ.Сити) один номер отдела продаж легитимно висит на всех его ЖК (277-99-01 на 9 проектах). Прежний порог dropCrowdedKeys (>4 карточек → «рекламный», выкинуть) прятал это, и окно «Найти дубли» не показывало проекты застройщика — systematically (6 таких номеров в данных). Решение владельца 05.07: порог по ТЕЛЕФОНУ (p:) убрать — клиент подтверждает склейку вручную («Это разные»), тихой ложной склейки нет; рекламные-подменыши и так отсекаются на сборе. Порог по АДРЕСУ (a:, бизнес-центр) оставлен. Проверено на AutoMoney: его номер у 3 карточек — все реально Автодом24, группа верная, монстр-группы снятие порога не даёт. Тесты переписаны со старого поведения на новое (unit + feature), 421/421. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
252 lines
13 KiB
PHP
252 lines
13 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('предложения: единый номер застройщика у многих проектов ГРУППИРУЕТ (порог людного номера снят)', function () {
|
|
// Кейс «Образцово» (реш. владельца 05.07): у застройщика (СМ.Сити) один номер отдела продаж
|
|
// висит на всех его ЖК. Прежний порог 4 прятал их как «рекламный номер». Теперь показываем
|
|
// группу — склейку клиент подтверждает вручную.
|
|
[$t, $u, $run] = dupSetup();
|
|
for ($i = 1; $i <= 9; $i++) {
|
|
dupProp($t, $run, 'ЖК '.$i, ['73912779901']); // единый номер застройщика на 9 проектах
|
|
}
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')
|
|
->assertOk()
|
|
->assertJsonCount(1, 'groups');
|
|
expect($resp->json('groups.0.competitors'))->toHaveCount(9);
|
|
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('телефон');
|
|
});
|
|
|
|
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('GET field/duplicate-groups?scope=cross — ловит предложение как дубль карточки поля', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$f = dupComp($t, $run, 'СМ-сити'); // в поле
|
|
$p = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Южный берег',
|
|
'dedup_key' => 'p:yb', 'box' => 'proposal',
|
|
]);
|
|
dupCall($t, $run, $f, '73912179941');
|
|
dupCall($t, $run, $p, '73912179941'); // общий телефон — связывает предложение с полем
|
|
|
|
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?scope=cross')->assertOk();
|
|
|
|
$resp->assertJsonCount(1, 'groups');
|
|
$comp = collect($resp->json('groups.0.competitors'))->keyBy('id');
|
|
expect($comp->keys()->map(fn ($k) => (int) $k)->sort()->values()->all())
|
|
->toBe(collect([$f->id, $p->id])->sort()->values()->all());
|
|
// в выдаче видно, кто из какого ящика (чтобы клиент понял: это предложение к карточке поля)
|
|
expect($comp[$p->id]['box'])->toBe('proposal')
|
|
->and($comp[$f->id]['box'])->toBe('field');
|
|
});
|
|
|
|
it('scope=cross — пара чисто-предложений (без карточки поля) НЕ показывается на экране поля', 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, '73912179941');
|
|
dupCall($t, $run, $p2, '73912179941');
|
|
|
|
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?scope=cross')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'groups');
|
|
});
|
|
|
|
it('GET field/duplicate-groups БЕЗ scope — по-прежнему один ящик (поле), предложения не мешают', function () {
|
|
[$t, $u, $run] = dupSetup();
|
|
$f = dupComp($t, $run, 'Поле A');
|
|
$p = AutopodborCompetitor::create([
|
|
'tenant_id' => $t->id, 'search_run_id' => $run->id, 'name' => 'Предл A',
|
|
'dedup_key' => 'p:a', 'box' => 'proposal',
|
|
]);
|
|
dupCall($t, $run, $f, '73912179941');
|
|
dupCall($t, $run, $p, '73912179941');
|
|
|
|
// без scope=cross поле и предложение НЕ группируются (обратная совместимость)
|
|
$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);
|
|
});
|