bcb327aa13
Инвариант «один проект = один источник»: при переносе проекта на новый источник снимаем привязку со всех прочих источников — проект больше не может висеть в двух конкурентах сразу (жёсткий баг с дублями). Плюс разбор ошибки в автоподборе показывает конкретную причину бэкенда вместо общей фразы «не удалось». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
9.4 KiB
PHP
150 lines
9.4 KiB
PHP
<?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');
|
||
});
|