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>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Перевод кодов регионов: Лидерра → поставщик crm.bp-gr.ru.
|
||||
*
|
||||
* Лидерра нумерует субъекты РФ по конституционному порядку (ст. 65), 1..89 —
|
||||
* см. {@see RussianRegions}: Красноярский край = 29, Архангельская обл. = 35.
|
||||
* Поставщик нумерует по автомобильным кодам (ГИБДД): Красноярский = 24,
|
||||
* Архангельская = 29. Без перевода Sync отправлял Лидерра-код «как есть»
|
||||
* (`regions => [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<int, int>
|
||||
*/
|
||||
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<int>|array<int|string, int|string> $liderraCodes
|
||||
* @return list<int>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\SupplierRegions;
|
||||
use Tests\TestCase;
|
||||
|
||||
// Бутстрапим приложение — mapToSupplier() пишет Log::warning при отбросе непереводимых.
|
||||
uses(TestCase::class);
|
||||
|
||||
// Regression: Лидерра нумерует субъекты по конституционному порядку (RussianRegions,
|
||||
// Красноярский=29), поставщик crm.bp-gr.ru — по автокодам ГИБДД (Красноярский=24,
|
||||
// Архангельск=29). Sync слал Лидерра-код как есть → у поставщика выбирался ЧУЖОЙ регион.
|
||||
// SupplierRegions::mapToSupplier переводит Лидерра-код → код поставщика.
|
||||
|
||||
it('translates Liderra constitutional codes to supplier (ГИБДД) codes', function (): void {
|
||||
expect(SupplierRegions::mapToSupplier([29]))->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)));
|
||||
});
|
||||
Reference in New Issue
Block a user