Files
portal/app/tests/Feature/Autopodbor/AutopodborManualApiTest.php
T
Дмитрий bcb327aa13 fix(autopodbor): проект принадлежит ровно одному источнику и понятная причина отказа
Инвариант «один проект = один источник»: при переносе проекта на новый источник
снимаем привязку со всех прочих источников — проект больше не может висеть в двух
конкурентах сразу (жёсткий баг с дублями). Плюс разбор ошибки в автоподборе
показывает конкретную причину бэкенда вместо общей фразы «не удалось».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:02:40 +03:00

150 lines
9.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\AutopodborCompetitor;
use App\Models\AutopodborRun;
use App\Models\AutopodborSource;
use App\Models\Project;
use App\Models\SystemSetting;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
beforeEach(fn () => 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');
});