feat(автоподбор): интерфейс движка CompetitorAgent + заглушка + binding

- CompetitorAgent interface: findCompetitors / studyCompetitor / resolveByName
- FakeCompetitorAgent: 4 конкурента, 5 источников, 1 кандидат по имени
- AutopodborServiceProvider: bind(CompetitorAgent → FakeCompetitorAgent)
- Регистрация провайдера в bootstrap/providers.php (Laravel 11+)
- Pest.php: extend TestCase для Unit/Autopodbor (контейнер в Unit-тестах)
- Тест: 1/1 PASS, 10 assertions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-28 13:28:56 +03:00
parent ec19a19132
commit cbae23ba86
6 changed files with 89 additions and 0 deletions
@@ -0,0 +1,16 @@
<?php
namespace App\Providers;
use App\Services\Autopodbor\Agent\CompetitorAgent;
use App\Services\Autopodbor\Agent\FakeCompetitorAgent;
use Illuminate\Support\ServiceProvider;
class AutopodborServiceProvider extends ServiceProvider
{
public function register(): void
{
// v1: заглушка. Реальный движок биндится здесь, когда будет готов.
$this->app->bind(CompetitorAgent::class, FakeCompetitorAgent::class);
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Services\Autopodbor\Agent;
use App\Services\Autopodbor\Agent\Dto\{FindCompetitorsRequest, FindCompetitorsResult, StudyCompetitorRequest, StudyCompetitorResult, ResolveByNameRequest, ResolveByNameResult};
interface CompetitorAgent
{
public function findCompetitors(FindCompetitorsRequest $r): FindCompetitorsResult;
public function studyCompetitor(StudyCompetitorRequest $r): StudyCompetitorResult;
public function resolveByName(ResolveByNameRequest $r): ResolveByNameResult;
}
@@ -0,0 +1,36 @@
<?php
namespace App\Services\Autopodbor\Agent;
use App\Services\Autopodbor\Agent\Dto\{FindCompetitorsRequest, FindCompetitorsResult, StudyCompetitorRequest, StudyCompetitorResult, ResolveByNameRequest, ResolveByNameResult};
final class FakeCompetitorAgent implements CompetitorAgent
{
public function findCompetitors(FindCompetitorsRequest $r): FindCompetitorsResult
{
return new FindCompetitorsResult([
['name' => 'Окна Комфорт', 'description' => 'Пластиковые окна и остекление балконов под ключ.', 'is_federal' => false, 'relevance_pct' => 100, 'site_url' => 'okna-komfort-kzn.ru', 'directory_urls' => ['https://2gis.ru/firm/1'], 'provenance' => ['via' => 'similar-pages']],
['name' => 'Пластика Окон', 'description' => 'Окна ПВХ, лоджии, входные группы.', 'is_federal' => false, 'relevance_pct' => 96, 'site_url' => 'plastika-okon-kzn.ru', 'directory_urls' => ['https://2gis.ru/firm/2'], 'provenance' => ['via' => 'similar-pages']],
['name' => 'Фабрика Окон', 'description' => 'Федеральная сеть окон ПВХ, филиал в регионе.', 'is_federal' => true, 'relevance_pct' => 84, 'site_url' => 'fabrika-okon.ru', 'directory_urls' => ['https://2gis.ru/firm/3'], 'provenance' => ['via' => 'similar-pages']],
['name' => 'Балкон-Сервис 16', 'description' => 'Остекление балконов; окна частично.', 'is_federal' => false, 'relevance_pct' => 61, 'site_url' => null, 'directory_urls' => ['https://yandex.ru/maps/4', 'https://2gis.ru/firm/4'], 'provenance' => ['via' => 'similar-pages']],
]);
}
public function studyCompetitor(StudyCompetitorRequest $r): StudyCompetitorResult
{
return new StudyCompetitorResult([
['signal_type' => 'site', 'identifier' => 'okna-komfort-kzn.ru', 'phone_kind' => null, 'provenance_url' => 'https://2gis.ru/firm/1', 'provenance_label' => '2ГИС — карточка компании'],
['signal_type' => 'site', 'identifier' => 'okna-komfort.pro', 'phone_kind' => null, 'provenance_url' => 'https://yandex.ru/maps/1', 'provenance_label' => 'Яндекс.Карты — сайт в контактах'],
['signal_type' => 'call', 'identifier' => '78432001122', 'phone_kind' => 'real', 'provenance_url' => 'https://2gis.ru/firm/1', 'provenance_label' => '2ГИС — карточка компании'],
['signal_type' => 'call', 'identifier' => '78432009988', 'phone_kind' => 'substitute', 'provenance_url' => 'https://okna-komfort-kzn.ru', 'provenance_label' => 'номер в шапке (коллтрекинг)'],
['signal_type' => 'call', 'identifier' => '79172001122', 'phone_kind' => 'real', 'provenance_url' => 'https://yandex.ru/maps/1', 'provenance_label' => 'Яндекс.Карты — карточка компании'],
]);
}
public function resolveByName(ResolveByNameRequest $r): ResolveByNameResult
{
return new ResolveByNameResult([
['name' => $r->name, 'description' => 'Найдено по названию (заглушка).', 'site_url' => 'okna-komfort-kzn.ru', 'directory_urls' => ['https://2gis.ru/firm/1'], 'provenance' => ['via' => 'name-search']],
]);
}
}
+1
View File
@@ -4,4 +4,5 @@ use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
App\Providers\AutopodborServiceProvider::class,
];
+4
View File
@@ -30,6 +30,10 @@ uses(SharesAdminPdo::class)->in('Feature');
pest()->extend(TestCase::class)->in('Browser');
// Unit/Autopodbor: часть тестов использует app() и requires Laravel-контейнер
// (в т.ч. FakeCompetitorAgentTest — резолвит CompetitorAgent через провайдер).
pest()->extend(TestCase::class)->in('Unit/Autopodbor');
/*
|--------------------------------------------------------------------------
| Expectations
@@ -0,0 +1,20 @@
<?php
use App\Services\Autopodbor\Agent\CompetitorAgent;
use App\Services\Autopodbor\Agent\Dto\{FindCompetitorsRequest, StudyCompetitorRequest, ResolveByNameRequest};
it('заглушка резолвится через контейнер и отдаёт данные', function () {
$agent = app(CompetitorAgent::class);
expect($agent)->toBeInstanceOf(\App\Services\Autopodbor\Agent\FakeCompetitorAgent::class);
$found = $agent->findCompetitors(new FindCompetitorsRequest(16, ['okna.ru'], [], true, 15));
expect(count($found->competitors))->toBeGreaterThan(0)
->and($found->competitors[0])->toHaveKeys(['name','relevance_pct','site_url']);
$study = $agent->studyCompetitor(new StudyCompetitorRequest(['name'=>'Окна Комфорт','site_url'=>'okna-komfort-kzn.ru'], 16));
expect($study->sources)->not->toBeEmpty()
->and($study->sources[0])->toHaveKeys(['signal_type','identifier','provenance_url']);
$resolve = $agent->resolveByName(new ResolveByNameRequest('Окна Комфорт', 16));
expect($resolve->candidates)->not->toBeEmpty();
});