Files
portal/app/tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php
T
Дмитрий 1dc696cef6 fix(supplier): перевод кодов регионов Лидерра→поставщик (конституционный→ГИБДД)
Лидерра нумерует субъекты по конституционному порядку (RussianRegions:
Красноярский=29), поставщик crm.bp-gr.ru — по автокодам ГИБДД (Красноярский=24,
Архангельск=29). Sync слал Лидерра-код как есть → поставщик выбирал ЧУЖОЙ регион
(заказчик выбрал Красноярский край — у поставщика встал Архангельск). На dev не
всплывало: проверяли на «вся РФ» (пустой regions).

Фикс: App\Support\SupplierRegions::mapToSupplier — карта 79 субъектов, построена
сверкой имён RussianRegions ↔ live-дерево формы «Добавить проект» поставщика
(recon 2026-05-21, node-key="id"). Перевод в единственной точке выхода —
SupplierPortalClient::toPayload (покрывает create/update/multiFlag). Тег остаётся
человекочитаемым именем Лидерры.

10 субъектов Лидерры поставщик не предлагает (Московская/Ленинградская/Крым/
Севастополь/ДНР/ЛНР/Запорожская/Херсонская/Ненецкий АО/ЯНАО) — их коды
отбрасываются с warning'ом (георфильтр для них у поставщика недоступен).

Тесты: SupplierRegionsTest (перевод/отброс/dedupe/биекция);
SupplierPortalClientRtProjectTest обновлён (regions [77]→[72] после перевода).

Проверено вживую на тест-сервере: проекты 14/15 пере-синхронизированы, доноры
12742042/12766120 у crm.bp-gr.ru → regions=24 (Красноярский), reverse=false.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:50:18 +03:00

226 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Exceptions\Supplier\SupplierClientException;
use App\Services\Supplier\Dto\SupplierProjectDto;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
/*
* rt-project-* contract tests.
*
* Контракт верифицирован live 2026-05-19 (Playwright MCP recon — см. план
* Task 1: создан LIDPOTOK_TEST_DELETE_ME на crm.bp-gr.ru, записаны сетевые
* запросы, проект удалён). Endpoints:
* POST /admin/visit/rt-project-save (JSON, конверт {status,message,result,id})
* POST /admin/visit/rt-project-delete (JSON, конверт {status,message,result})
* GET /admin/visit/rt-projects-load?src=none
*
* Tests use Http::fake — отделяем контракт SupplierPortalClient от реального портала.
*/
beforeEach(function (): void {
Cache::store('redis')->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:"<string>"}', 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, 'Проект не найден');
});