diff --git a/app/app/Services/Supplier/SupplierPortalClient.php b/app/app/Services/Supplier/SupplierPortalClient.php index 39cd3282..2afe7e46 100644 --- a/app/app/Services/Supplier/SupplierPortalClient.php +++ b/app/app/Services/Supplier/SupplierPortalClient.php @@ -9,6 +9,7 @@ use App\Exceptions\Supplier\SupplierClientException; use App\Exceptions\Supplier\SupplierTransientException; use App\Jobs\Supplier\RefreshSupplierSessionJob; use App\Services\Supplier\Dto\SupplierProjectDto; +use App\Support\SupplierRegions; use Carbon\CarbonInterface; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Factory as HttpFactory; @@ -477,7 +478,10 @@ class SupplierPortalClient 'srcseg' => false, 'limit' => $dto->limit, 'workdays' => $workdays, - 'regions' => $dto->regions, + // DTO несёт Лидерра-коды (конституционный порядок); поставщик ждёт + // свои коды (ГИБДД). Без перевода уходил чужой регион (Красноярский 29 + // → Архангельск 29). См. App\Support\SupplierRegions. + 'regions' => SupplierRegions::mapToSupplier($dto->regions), 'regions_reverse' => $dto->regionsReverse, 'status' => $dto->status === 'active', 'show' => true, diff --git a/app/app/Support/SupplierRegions.php b/app/app/Support/SupplierRegions.php new file mode 100644 index 00000000..d699831a --- /dev/null +++ b/app/app/Support/SupplierRegions.php @@ -0,0 +1,162 @@ + [29]` для Красноярского), а поставщик понимал его как СВОЙ № 29 = + * Архангельск → у поставщика выбирался ЧУЖОЙ регион. На dev не всплывало — + * проверяли на «вся РФ» (пустой regions). + * + * Карта построена сверкой имён {@see RussianRegions::CODE_TO_NAME} ↔ live-дерево + * регионов формы «Добавить проект» поставщика (recon 2026-05-21: node-key="id", + * 79 субъектов-листьев). Все 79 кодов поставщика покрыты (биекция на 79). + * + * 10 субъектов Лидерры поставщик НЕ предлагает (нет в дереве) — их коды + * отбрасываются при переводе (с warning'ом): Московская обл. (56), + * Ленинградская обл. (53), Крым (13), Севастополь (84), ДНР (6), ЛНР (14), + * Запорожская (43), Херсонская (79), Ненецкий АО (86), Ямало-Ненецкий АО (89). + * Если у проекта это был ЕДИНСТВЕННЫЙ регион — у поставщика проект окажется без + * георфильтра (вся РФ). Это ограничение покрытия поставщика, не баг перевода. + */ +final class SupplierRegions +{ + /** + * Лидерра-код (конституционный 1..89) => код поставщика (ГИБДД). + * + * @var array + */ + public const LIDERRA_TO_SUPPLIER = [ + // Республики + 1 => 1, // Республика Адыгея + 2 => 4, // Республика Алтай + 3 => 2, // Республика Башкортостан + 4 => 3, // Республика Бурятия + 5 => 5, // Республика Дагестан + 7 => 6, // Республика Ингушетия + 8 => 7, // Кабардино-Балкарская Республика + 9 => 8, // Республика Калмыкия + 10 => 9, // Карачаево-Черкесская Республика + 11 => 10, // Республика Карелия + 12 => 11, // Республика Коми + 15 => 12, // Республика Марий Эл + 16 => 13, // Республика Мордовия + 17 => 14, // Республика Саха (Якутия) + 18 => 15, // Республика Северная Осетия — Алания + 19 => 16, // Республика Татарстан + 20 => 17, // Республика Тыва + 21 => 18, // Удмуртская Республика + 22 => 19, // Республика Хакасия + 23 => 20, // Чеченская Республика + 24 => 21, // Чувашская Республика + // Края + 25 => 22, // Алтайский край + 26 => 75, // Забайкальский край + 27 => 41, // Камчатский край + 28 => 23, // Краснодарский край + 29 => 24, // Красноярский край + 30 => 59, // Пермский край + 31 => 25, // Приморский край + 32 => 26, // Ставропольский край + 33 => 27, // Хабаровский край + // Области + 34 => 28, // Амурская область + 35 => 29, // Архангельская область + 36 => 30, // Астраханская область + 37 => 31, // Белгородская область + 38 => 32, // Брянская область + 39 => 33, // Владимирская область + 40 => 34, // Волгоградская область + 41 => 35, // Вологодская область + 42 => 36, // Воронежская область + 44 => 37, // Ивановская область + 45 => 38, // Иркутская область + 46 => 39, // Калининградская область + 47 => 40, // Калужская область + 48 => 42, // Кемеровская область + 49 => 43, // Кировская область + 50 => 44, // Костромская область + 51 => 45, // Курганская область + 52 => 46, // Курская область + 54 => 48, // Липецкая область + 55 => 49, // Магаданская область + 57 => 51, // Мурманская область + 58 => 52, // Нижегородская область + 59 => 53, // Новгородская область + 60 => 54, // Новосибирская область + 61 => 55, // Омская область + 62 => 56, // Оренбургская область + 63 => 57, // Орловская область + 64 => 58, // Пензенская область + 65 => 60, // Псковская область + 66 => 61, // Ростовская область + 67 => 62, // Рязанская область + 68 => 63, // Самарская область + 69 => 64, // Саратовская область + 70 => 65, // Сахалинская область + 71 => 66, // Свердловская область + 72 => 67, // Смоленская область + 73 => 68, // Тамбовская область + 74 => 69, // Тверская область + 75 => 70, // Томская область + 76 => 71, // Тульская область + 77 => 72, // Тюменская область + 78 => 73, // Ульяновская область + 80 => 74, // Челябинская область + 81 => 76, // Ярославская область + // Города федерального значения + 82 => 77, // Москва + 83 => 78, // Санкт-Петербург + // Автономная область / округа + 85 => 79, // Еврейская автономная область + 87 => 86, // Ханты-Мансийский автономный округ — Югра + 88 => 87, // Чукотский автономный округ + ]; + + /** + * Переводит Лидерра-коды регионов в коды поставщика. Неизвестные (нет у + * поставщика) отбрасываются с warning'ом; sentinel 0 («Вся РФ») игнорируется. + * Результат — уникальные коды поставщика по возрастанию. + * + * @param list|array $liderraCodes + * @return list + */ + public static function mapToSupplier(array $liderraCodes): array + { + $out = []; + $dropped = []; + + foreach ($liderraCodes as $code) { + $code = (int) $code; + if ($code === 0) { + continue; // sentinel «Вся РФ» + } + if (isset(self::LIDERRA_TO_SUPPLIER[$code])) { + $out[self::LIDERRA_TO_SUPPLIER[$code]] = true; + } else { + $dropped[] = $code; + } + } + + if ($dropped !== []) { + Log::warning('supplier.regions.unmapped', [ + 'liderra_codes' => $dropped, + 'note' => 'supplier does not offer these subjects — geo-filter dropped for them', + ]); + } + + $codes = array_keys($out); + sort($codes); + + return $codes; + } +} diff --git a/app/tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php b/app/tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php index 93fd9476..6ad7e14f 100644 --- a/app/tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php +++ b/app/tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php @@ -99,7 +99,9 @@ it('saveProject maps signalType call → type:"calls" and B2 → srcbl=true (sin && $request['srcrt'] === false && $request['srcbl'] === true && $request['srcmt'] === false - && $request['regions'] === [77] + // Лидерра-код 77 (Тюменская обл., конституционный порядок) переводится + // в код поставщика 72 (ГИБДД). См. App\Support\SupplierRegions. + && $request['regions'] === [72] && $request['regions_reverse'] === true && $request['status'] === false; }); diff --git a/app/tests/Unit/Supplier/SupplierRegionsTest.php b/app/tests/Unit/Supplier/SupplierRegionsTest.php new file mode 100644 index 00000000..525fe395 --- /dev/null +++ b/app/tests/Unit/Supplier/SupplierRegionsTest.php @@ -0,0 +1,49 @@ +toBe([24]); // Красноярский край + expect(SupplierRegions::mapToSupplier([35]))->toBe([29]); // Архангельская обл. + expect(SupplierRegions::mapToSupplier([24]))->toBe([21]); // Чувашская Республика + expect(SupplierRegions::mapToSupplier([82]))->toBe([77]); // Москва + expect(SupplierRegions::mapToSupplier([83]))->toBe([78]); // Санкт-Петербург +}); + +it('returns empty for all-Russia (no regions)', function (): void { + expect(SupplierRegions::mapToSupplier([]))->toBe([]); +}); + +it('ignores sentinel 0 (Вся РФ)', function (): void { + expect(SupplierRegions::mapToSupplier([0]))->toBe([]); +}); + +it('drops regions the supplier does not offer', function (): void { + // Поставщик НЕ предлагает: Московская (56), Ленинградская (53), Крым (13), новые территории. + expect(SupplierRegions::mapToSupplier([56]))->toBe([]); // Московская обл. + expect(SupplierRegions::mapToSupplier([53]))->toBe([]); // Ленинградская обл. + expect(SupplierRegions::mapToSupplier([13]))->toBe([]); // Крым + // mixed: оставляем переводимые, отбрасываем непереводимые + expect(SupplierRegions::mapToSupplier([29, 56]))->toBe([24]); // Красноярский kept, Московская dropped +}); + +it('dedupes and sorts supplier codes', function (): void { + // 35→29 (Архангельск), 29→24 (Красноярский), дубль 35 → unique+sorted [24,29] + expect(SupplierRegions::mapToSupplier([35, 29, 35]))->toBe([24, 29]); +}); + +it('every map entry points to a distinct supplier code (no collisions)', function (): void { + $targets = array_values(SupplierRegions::LIDERRA_TO_SUPPLIER); + expect(count($targets))->toBe(count(array_unique($targets))); +});