Files
portal/app/tests/Feature/Autopodbor/AutopodborProposalsApiTest.php
T
Дмитрий 4387333118 feat(Конкурентное поле): рабочее место конкуренты→источники→проекты (поверх автоподбора)
Фича «Конкурентное поле» на dev до уровня прототипа 2026-06-29-konkurentnoe-pole-proto.html.

Данные: box (proposal|field) на competitors+sources; phone_type city/mobile/tollfree рядом
с phone_kind (вариант C). 3 миграции, дефолты тарифов 300/50.

API (AutopodborController): GET /field (+счётчики), GET /proposals, PATCH/DELETE competitors
и sources с гвардами активного проекта, переключение box, POST /competitors/manual (+directory_urls),
competitor(id) обогащён box+project-статусом; projectStatus отдаёт limit/delivered/days/regions.
Смена источника проекта = PATCH /api/projects/{id} (реальный гвард слепка §14.10).

Фронт: FieldWorkspaceScreen/FieldCompetitorScreen/FieldProposalsScreen/FieldManualCompetitorScreen
+ field-shared.css (Forest) + AutopodborServicesPanel в Биллинге. Дословно по прототипу: подзаголовки,
баннер предложений, баннер правил времени 18:00 МСК, Справочник 2ГИС·Яндекс, статус проекта
5/день·заявки, окна сбора с ценами 300/50 + «что известно», полные формы. Пункт меню «Конкурентное поле».

Тесты: backend автоподбор 80/80, фронт автоподбор 49/49. Движок шага 2 = заглушка FakeCompetitorAgent.
OmegaDemoFieldSeeder — только для визуальной проверки (НЕ на прод).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 04:18:46 +03:00

47 lines
2.6 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\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
it('GET /api/autopodbor/proposals — отдаёт только конкурентов-предложения, сорт по похожести', 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' => 'search', 'status' => 'done', 'region_code' => 16, 'params' => []]);
AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'name' => 'Низкая', 'dedup_key' => 'low', 'box' => 'proposal', 'relevance_pct' => 40]);
AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'name' => 'Высокая', 'dedup_key' => 'high', 'box' => 'proposal', 'relevance_pct' => 95]);
// в поле — не предложение, не должен попасть
AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'name' => 'В поле', 'dedup_key' => 'fld', 'box' => 'field', 'relevance_pct' => 100]);
$data = $this->actingAs($user)->getJson('/api/autopodbor/proposals')
->assertOk()
->assertJsonStructure(['data' => [['id', 'name', 'box', 'relevance_pct']]])
->json('data');
expect($data)->toHaveCount(2)
->and($data[0]['name'])->toBe('Высокая') // сорт по похожести
->and($data[1]['name'])->toBe('Низкая');
});
it('GET /api/autopodbor/proposals — чужой тенант своих не видит', 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' => 'search', 'status' => 'done', 'region_code' => 16, 'params' => []]);
AutopodborCompetitor::create(['tenant_id' => $tenant->id, 'search_run_id' => $run->id, 'name' => 'Чужой', 'dedup_key' => 'a', 'box' => 'proposal']);
$other = User::factory()->create(['tenant_id' => Tenant::factory()->create()->id]);
$data = $this->actingAs($other)->getJson('/api/autopodbor/proposals')->assertOk()->json('data');
expect($data)->toHaveCount(0);
});