put('supplier:session', [ 'phpsessid' => 'test-session', 'csrf' => 'test-csrf', ], now()->addHour()); config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']); }); it('saveProject POSTs to /admin/visit/rt-project-save with JSON body and parses id from envelope', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response( ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '12721245'], 200, ), ]); $dto = new SupplierProjectDto( platform: 'B1', signalType: 'site', uniqueKey: 'lidpotok-test.local', limit: 100, workdays: [1, 2, 3, 4, 5, 6, 7], regions: [], regionsReverse: false, status: 'active', ); $externalId = app(SupplierPortalClient::class)->saveProject($dto); expect($externalId)->toBe(12721245); Http::assertSent(function (Request $request): bool { $expectsB1 = $request['srcrt'] === true && $request['srcbl'] === false && $request['srcmt'] === false; return $request->method() === 'POST' && $request->url() === 'https://crm.bp-gr.ru/admin/visit/rt-project-save' && $request->hasHeader('Content-Type', 'application/json') && $request['id'] === 0 && $request['tag'] === '_lidpotok' && $request['name'] === 'lidpotok-test.local' && $request['content'] === 'lidpotok-test.local' && $request['type'] === 'hosts' && $expectsB1 && $request['limit'] === 100 && $request['workdays'] === ['1', '2', '3', '4', '5', '6', '7'] && $request['regions_reverse'] === false && $request['status'] === true; }); }); it('saveProject maps signalType call → type:"calls" and B2 → srcbl=true (single-true)', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response( ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '12721244'], 200, ), ]); $dto = new SupplierProjectDto( platform: 'B2', signalType: 'call', uniqueKey: '79991112233', limit: 50, workdays: [1, 2, 3], regions: [77], regionsReverse: true, status: 'paused', ); app(SupplierPortalClient::class)->saveProject($dto); Http::assertSent(function (Request $request): bool { return $request['type'] === 'calls' && $request['srcrt'] === false && $request['srcbl'] === true && $request['srcmt'] === false // Лидерра-код 77 (Тюменская обл., конституционный порядок) переводится // в код поставщика 72 (ГИБДД). См. App\Support\SupplierRegions. && $request['regions'] === [72] && $request['regions_reverse'] === true && $request['status'] === false; }); }); it('updateProject POSTs to /admin/visit/rt-project-save with id:N (same endpoint as save)', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response( ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '12721245'], 200, ), ]); $dto = new SupplierProjectDto( platform: 'B3', signalType: 'sms', uniqueKey: 'KEYWORD', limit: 25, workdays: [1, 5], regions: [], regionsReverse: false, status: 'active', ); app(SupplierPortalClient::class)->updateProject(12721245, $dto); Http::assertSent(function (Request $request): bool { return $request->method() === 'POST' && $request->url() === 'https://crm.bp-gr.ru/admin/visit/rt-project-save' && $request['id'] === 12721245 && $request['type'] === 'sms' && $request['srcmt'] === true; }); }); it('deleteProject POSTs to /admin/visit/rt-project-delete with JSON {id:""}', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response( ['status' => 'OK', 'message' => '', 'result' => null], 200, ), ]); app(SupplierPortalClient::class)->deleteProject(12721245); Http::assertSent(function (Request $request): bool { return $request->method() === 'POST' && $request->url() === 'https://crm.bp-gr.ru/admin/visit/rt-project-delete' && $request->hasHeader('Content-Type', 'application/json') && $request['id'] === '12721245'; }); }); it('listProjects extracts projects[] from the envelope and returns raw rows', function (): void { // Verified live 2026-05-19: ответ — конверт {projects:[...], tags, users, ...}, // НЕ голый массив. listProjects извлекает projects. Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response([ 'projects' => [ ['id' => '12721245', 'tag' => '_lidpotok', 'name' => 'B3_LIDPOTOK', 'type' => 'hosts', 'content' => 'foo.com'], ], 'tags' => [], 'users' => [], ], 200), ]); $list = app(SupplierPortalClient::class)->listProjects(); expect($list)->toHaveCount(1); expect($list[0]['id'])->toBe('12721245'); expect($list[0]['name'])->toBe('B3_LIDPOTOK'); Http::assertSent(function (Request $request): bool { return $request->method() === 'GET' && str_contains($request->url(), '/admin/visit/rt-projects-load') && $request->data() === ['src' => 'none']; }); }); it('listProjects returns empty array when envelope has no projects key', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(['tags' => []], 200), ]); expect(app(SupplierPortalClient::class)->listProjects())->toBe([]); }); it('saveProject throws SupplierClientException on HTTP 200 + status:"Error"', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response( ['status' => 'Error', 'message' => 'Лимит недостаточен!', 'result' => null], 200, ), ]); $dto = new SupplierProjectDto( platform: 'B1', signalType: 'site', uniqueKey: 'rejected.local', limit: 1, workdays: [1, 2, 3, 4, 5, 6, 7], regions: [], regionsReverse: false, status: 'active', ); expect(fn () => app(SupplierPortalClient::class)->saveProject($dto)) ->toThrow(SupplierClientException::class, 'Лимит недостаточен!'); }); it('deleteProject throws SupplierClientException on HTTP 200 + status:"Error"', function (): void { Http::fake([ 'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response( ['status' => 'Error', 'message' => 'Проект не найден', 'result' => null], 200, ), ]); expect(fn () => app(SupplierPortalClient::class)->deleteProject(99999999)) ->toThrow(SupplierClientException::class, 'Проект не найден'); });