Queue::fake()); it('manual-study по сайту создаёт ручного конкурента и study-прогон', function () { $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); $user = User::factory()->create(['tenant_id' => $tenant->id]); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_study_rub'], ['value' => '1', 'type' => 'decimal']); $this->actingAs($user)->postJson('/api/autopodbor/manual-study', [ 'site_url' => 'https://okna-komfort-kzn.ru/contacts', 'region_code' => 16, ])->assertCreated()->assertJsonPath('data.kind', 'study'); DB::statement('SET app.current_tenant_id = '.$tenant->id); expect(AutopodborCompetitor::where('tenant_id', $tenant->id)->where('origin', 'manual')->exists())->toBeTrue(); }); it('manual-study без названия и без сайта → 422', function () { $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); $user = User::factory()->create(['tenant_id' => $tenant->id]); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_study_rub'], ['value' => '1', 'type' => 'decimal']); $this->actingAs($user)->postJson('/api/autopodbor/manual-study', ['region_code' => 16]) ->assertStatus(422); }); it('sources/manual добавляет источник изученному конкуренту', function () { $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' => []]); $comp = AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'study_run_id' => $run->id, 'studied_at' => now(), 'name' => 'Окна Комфорт', 'origin' => 'auto', 'dedup_key' => 'okna']); $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $comp->id, 'raw' => 'okna-komfort.ru', ])->assertCreated() ->assertJsonPath('data.signal_type', 'site') ->assertJsonPath('data.box', 'field'); // ручной источник — сразу «в работу», не в предложения expect(AutopodborSource::where('competitor_id', $comp->id)->where('identifier', 'okna-komfort.ru')->first()) ->box->toBe('field'); }); // ——— #4: защита от дублей источника ——— function makeStudiedCompetitors(int $tenantId): array { $run = AutopodborRun::create(['tenant_id' => $tenantId, 'kind' => 'study', 'status' => 'done', 'region_code' => 16, 'params' => []]); $a = AutopodborCompetitor::create(['tenant_id' => $tenantId, 'search_run_id' => $run->id, 'study_run_id' => $run->id, 'studied_at' => now(), 'name' => 'Конкурент А', 'origin' => 'auto', 'dedup_key' => 'a']); $b = AutopodborCompetitor::create(['tenant_id' => $tenantId, 'search_run_id' => $run->id, 'study_run_id' => $run->id, 'studied_at' => now(), 'name' => 'Конкурент Б', 'origin' => 'auto', 'dedup_key' => 'b']); return [$run, $a, $b]; } it('sources/manual — такой же источник у другого конкурента → 409 duplicate_source', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); DB::statement('SET app.current_tenant_id = '.$tenant->id); [$run, $a, $b] = makeStudiedCompetitors($tenant->id); AutopodborSource::create(['tenant_id' => $tenant->id, 'competitor_id' => $a->id, 'study_run_id' => $run->id, 'signal_type' => 'site', 'identifier' => 'dubel-test.ru', 'dedup_key' => 's:dubel-test.ru', 'box' => 'field']); $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $b->id, 'raw' => 'dubel-test.ru', ])->assertStatus(409) ->assertJsonPath('error', 'duplicate_source') ->assertJsonPath('duplicate.competitor_name', 'Конкурент А') ->assertJsonPath('duplicate.has_project', false); }); it('sources/manual — по источнику уже есть проект → 409 с has_project', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); DB::statement('SET app.current_tenant_id = '.$tenant->id); [$run, $a, $b] = makeStudiedCompetitors($tenant->id); $project = Project::factory()->asSiteSignal('dubel-test.ru')->create(['tenant_id' => $tenant->id]); $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $b->id, 'raw' => 'dubel-test.ru', ])->assertStatus(409) ->assertJsonPath('error', 'duplicate_source') ->assertJsonPath('duplicate.has_project', true) ->assertJsonPath('duplicate.project_id', $project->id); }); it('sources/manual — force+absorb переносит источник и привязывает существующий проект, старый источник удалён', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); DB::statement('SET app.current_tenant_id = '.$tenant->id); [$run, $a, $b] = makeStudiedCompetitors($tenant->id); $srcA = AutopodborSource::create(['tenant_id' => $tenant->id, 'competitor_id' => $a->id, 'study_run_id' => $run->id, 'signal_type' => 'site', 'identifier' => 'dubel-test.ru', 'dedup_key' => 's:dubel-test.ru', 'box' => 'field']); $project = Project::factory()->asSiteSignal('dubel-test.ru')->create(['tenant_id' => $tenant->id]); $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $b->id, 'raw' => 'dubel-test.ru', 'force' => true, 'absorb_source_id' => $srcA->id, ])->assertCreated()->assertJsonPath('data.box', 'field'); $newSrc = AutopodborSource::where('competitor_id', $b->id)->where('identifier', 'dubel-test.ru')->first(); expect($newSrc->created_project_id)->toBe($project->id); // проект привязан к новому источнику expect(AutopodborSource::find($srcA->id))->toBeNull(); // старый источник-дубль убран expect($project->fresh()->name)->toBe('Конкурент Б'); // имя проекта обновлено на нового конкурента }); it('sources/manual — перенос проекта: проект остаётся привязан РОВНО к одному источнику', function () { // Регресс на жёсткий баг с дублями: раньше проект мог «висеть» в двух конкурентах сразу // (created_project_id стоял и у старого, и у нового источника). $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); DB::statement('SET app.current_tenant_id = '.$tenant->id); [$run, $a, $b] = makeStudiedCompetitors($tenant->id); $project = Project::factory()->asSiteSignal('dubel-test.ru')->create(['tenant_id' => $tenant->id]); $srcA = AutopodborSource::create(['tenant_id' => $tenant->id, 'competitor_id' => $a->id, 'study_run_id' => $run->id, 'signal_type' => 'site', 'identifier' => 'dubel-test.ru', 'dedup_key' => 's:dubel-test.ru', 'box' => 'field', 'created_project_id' => $project->id]); // переносим тот же источник в конкурента Б (force, БЕЗ absorb) $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $b->id, 'raw' => 'dubel-test.ru', 'force' => true, ])->assertCreated(); $srcB = AutopodborSource::where('competitor_id', $b->id)->where('identifier', 'dubel-test.ru')->first(); expect($srcB->created_project_id)->toBe($project->id); // новый источник владеет проектом expect($srcA->fresh()->created_project_id)->toBeNull(); // старый больше НЕ привязан (инвариант) expect(AutopodborSource::where('tenant_id', $tenant->id)->where('created_project_id', $project->id)->count())->toBe(1); // ровно один владелец }); it('sources/manual телефоном создаёт call-источник', function () { $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' => []]); $comp = AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'study_run_id' => $run->id, 'studied_at' => now(), 'name' => 'Окна', 'origin' => 'auto', 'dedup_key' => 'okna2']); $this->actingAs($user)->postJson('/api/autopodbor/sources/manual', [ 'competitor_id' => $comp->id, 'raw' => '+7 (843) 200-11-22', ])->assertCreated()->assertJsonPath('data.signal_type', 'call'); });