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); });