Files
anal-front/docs/backend-endpoints-competence-map-association-map-from-layouts.md
Gleb Korotkiy 20a3e0a843
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 41s
CI/CD Pipeline / Build and Deploy Dev via Compose (push) Has been skipped
feat: add maps pages
2026-06-24 21:46:08 +03:00

83 KiB
Raw Permalink Blame History

Backend Endpoints Competence Map Association Map

Назначение: спецификация backend-контракта для страниц:

  • Все сервисы / Карта компетенций / Карта кластеров (/new-map);
  • Все сервисы / Карта компетенций / Карта технопарков (/new-map/technoparks);
  • Все сервисы / Карта компетенций / Карта ОЭЗ (/new-map/sez).

Документ нужен как задача для backend-разработки. Цель: реализовать endpoint-ы, которые возвращают все серверное состояние новых страниц карты по участию организаций ОПК в инновационных объединениях:

  • значения фильтров;
  • доступные разделы карты;
  • доступные типы отображения карты;
  • выбранные субъекты РФ или федеральные округа как фильтр карты;
  • KPI-карточки;
  • данные для раскраски и tooltip-ов карты субъектов РФ;
  • данные для раскраски и tooltip-ов карты федеральных округов;
  • список объединений, в числе участников которых есть организации ОПК;
  • рейтинг субъектов РФ;
  • счетчик и перечень организаций ОПК для табличного представления.

Документ является дополнением к docs/backend-endpoints-competence-map-analytics-from-layouts.md. Он не заменяет endpoint-ы аналитической панели /dashboard.

Принципы контракта

  • snapshot страницы карты должен возвращаться одним page-level endpoint-ом;
  • справочники фильтров должны возвращаться отдельным endpoint-ом;
  • табличный перечень организаций должен возвращаться отдельным collection endpoint-ом, потому что это потенциально большой массив и он открывается отдельным состоянием страницы;
  • контракт описывает серверное состояние страницы, а не конкретную визуальную реализацию UI;
  • frontend может локально решать, показывать association_type как route tabs или navigation links, какие иконки использовать, как отрисовывать SVG-карту и как скроллить таблицы;
  • backend должен возвращать все данные, необходимые для всех серверно значимых состояний страницы: фильтры, выбранные субъекты/округа, KPI, строки рейтинга, строки списка объединений, строки таблицы организаций, id сущностей и значения для drilldown;
  • все значения должны возвращаться в raw-формате: number, string, boolean, null;
  • проценты должны возвращаться числом 0..100, а не долей 0..1;
  • даты должны возвращаться в ISO 8601 с timezone;
  • каждый объект, который может использоваться для drilldown или ссылки, должен иметь стабильный id;
  • все массивы должны возвращаться уже отсортированными, если для блока задан серверный порядок;
  • все KPI должны иметь стабильный code;
  • 0 должен возвращаться как валидное значение, а не как null, строка или ;
  • null допустим только для реально отсутствующего или неприменимого значения;
  • backend должен обеспечивать консистентность KPI, карты, рейтинга, списка объединений и таблицы организаций внутри одного набора фильтров.

Важное ограничение по числам в примерах:

  • числовые значения в JSON-примерах взяты из референсных экранов и текущих mock-данных страницы;
  • реальные значения должны быть рассчитаны по утвержденной backend-методике;
  • если методика конкретного показателя не утверждена, ее нужно зафиксировать до реализации endpoint-а;
  • если число на референсном экране противоречит смыслу поля, backend-контракт должен сохранять корректную семантику поля, а не повторять ошибочное или placeholder-значение макета.

REST и HTTP правила

Модель ресурсов

Endpoint-ы описывают серверные представления ресурсов, а не действия UI.

В рамках этих страниц есть три ресурса:

  • /api/v1/competence-map/association-map/filters/ - справочники и доступные значения фильтров;
  • /api/v1/competence-map/association-map/ - snapshot карты для выбранных фильтров;
  • /api/v1/competence-map/association-map/organizations/ - табличный перечень организаций ОПК для выбранных фильтров.

Требования:

  • не добавлять action-style endpoint-ы вида /getMap, /loadMapFilters, /selectRegion;
  • не использовать frontend path /new-map в API path;
  • association_type является query-параметром одного ресурса карты, а не отдельным URI-сегментом;
  • выбранные на карте субъекты РФ и федеральные округа являются query-параметрами snapshot-а, а не отдельными action endpoint-ами;
  • path должен оставаться стабильным в рамках /api/v1; breaking changes требуют новой версии API.

HTTP методы

Для всех endpoint-ов используется только GET.

Причина:

  • запросы только читают данные;
  • запросы не меняют серверное состояние;
  • результат полностью определяется URI, query params, правами пользователя и актуальностью данных.

GET request body не используется. Все параметры выбора представления передаются только через query params и headers.

POST для этих экранов не нужен. Его можно вводить только для отдельных будущих сценариев:

  • создание export job;
  • сохранение пользовательского набора фильтров;
  • тяжелый search/report request, который не помещается в URL и имеет отдельный жизненный цикл.

Media type

Request:

Accept: application/json

Успешный response:

Content-Type: application/json; charset=utf-8
Content-Language: ru-RU

Error response:

Content-Type: application/problem+json; charset=utf-8

Query params

Для /association-map/filters/ и /association-map/ используется единый набор query params:

  • association_type: clusters | technoparks | sez, optional, default clusters;
  • map_view_mode: regions | federal_districts, optional, default regions;
  • federal_district_id: string, optional;
  • region_id: string, optional;
  • federal_district_ids: array of string, optional;
  • region_ids: array of string, optional;
  • industry_id: string, optional;
  • integrated_structure_id: string, optional;
  • organization_id: string, optional;
  • competence_id: string, optional.

Для /association-map/organizations/ дополнительно используются query params:

  • limit: integer, optional, default 500, maximum 1000;
  • offset: integer, optional, default 0;
  • sort_field: string, optional, default serial_number;
  • sort_direction: asc | desc, optional, default asc.

Правила:

  • если фильтр не выбран, параметр лучше не передавать;
  • если frontend передал пустую строку, backend должен нормализовать ее в null или пустой массив в applied_filters;
  • неизвестные query params должны возвращать 400 Bad Request, чтобы опечатки не игнорировались молча;
  • повтор одного и того же query param запрещен, кроме массивов region_ids и federal_district_ids;
  • массивы передаются в canonical form как repeated query params:
region_ids=RU-MOS&region_ids=RU-MOW
federal_district_ids=cfo&federal_district_ids=pfo
  • значения id сравниваются как case-sensitive строки;
  • все примененные значения backend возвращает в applied_filters уже нормализованными;
  • region_id и элементы region_ids для карты должны соответствовать SVG-id субъектов РФ в формате RU-*, например RU-MOS, RU-MOW, RU-TA;
  • federal_district_id и элементы federal_district_ids должны соответствовать стабильным id федеральных округов, например cfo, pfo, ufo, sfo, dfo, nwfo, skfo, yufo;
  • если region_id передан вместе с federal_district_id, backend должен проверить, что регион входит в указанный федеральный округ;
  • нельзя одновременно передавать region_id и region_ids;
  • нельзя одновременно передавать federal_district_id и federal_district_ids;
  • нельзя одновременно передавать region_ids и federal_district_ids;
  • нельзя одновременно передавать одиночные geography-фильтры (region_id, federal_district_id) и multi-selection-фильтры карты (region_ids, federal_district_ids);
  • если фильтры валидны, но данных по ним нет, возвращается 200 OK с пустыми массивами;
  • если значение фильтра не существует или недоступно пользователю, возвращается ошибка валидации.

Canonical examples:

GET /api/v1/competence-map/association-map/filters/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=technoparks&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW
GET /api/v1/competence-map/association-map/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo
GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&region_ids=RU-MOS&limit=500&offset=0

Не использовать как canonical form:

GET /api/v1/competence-map/association-map/?association_type=clusters&region_id=&federal_district_id=
GET /api/v1/competence-map/association-map/?association_type=clusters&region_id=RU-MOS&region_ids=RU-MOW

Первый request можно поддержать для совместимости, но backend все равно должен вернуть пустые значения как null или [] в applied_filters. Второй request должен возвращать 400 Bad Request, потому что одиночный фильтр и multi-selection фильтр конфликтуют.

Headers и трассировка

Каждый response должен содержать:

X-Request-Id: <request id>

Требования:

  • если frontend передал X-Request-Id, backend должен использовать его или вернуть связанный id;
  • если id не передан, backend генерирует его сам;
  • этот id должен попадать в backend logs, чтобы можно было связать ошибку UI и серверный запрос;
  • технические debug-поля не добавляются в JSON payload, для них используются headers и логи.

Авторизация и права доступа

Endpoint-ы должны учитывать права текущего пользователя.

Правила:

  • 401 Unauthorized возвращается, если пользователь не аутентифицирован;
  • 403 Forbidden возвращается, если пользователь аутентифицирован, но не имеет доступа к странице или разделу;
  • для 401 Unauthorized backend должен вернуть WWW-Authenticate, если это предусмотрено текущей схемой аутентификации;
  • если пользователь имеет доступ к странице, но часть справочников или данных ему недоступна, backend должен отфильтровать эти данные на сервере;
  • filters не должен раскрывать id сущностей, к которым у пользователя нет доступа;
  • disabled = true можно использовать только для значений, которые пользователь видит, но которые неприменимы при текущих фильтрах;
  • значение фильтра, недоступное пользователю, обрабатывается как invalid filter value и не должно раскрывать, существует ли такая сущность в системе.

Кэширование и условные GET

Данные зависят от пользователя, прав доступа и фильтров, поэтому shared caching должен быть запрещен.

Recommended headers для успешных responses:

Cache-Control: private, max-age=60
Vary: Authorization, Accept
ETag: "<user-and-query-specific-etag>"
Last-Modified: <data_actual_at as HTTP-date>

Требования:

  • ETag должен учитывать пользователя, права доступа, endpoint path, query params и версию/актуальность данных;
  • Last-Modified должен соответствовать актуальности данных, а не времени генерации response;
  • backend должен поддерживать If-None-Match и может вернуть 304 Not Modified, если представление не изменилось;
  • при 304 Not Modified тело response не возвращается;
  • для персональных данных нельзя использовать Cache-Control: public;
  • если backend пока не готов поддерживать validators, нужно явно вернуть Cache-Control: no-store.

Актуальность и методика расчета

Response содержит два разных времени:

  • generated_at - когда backend сформировал текущий response;
  • data_actual_at - на какой момент актуальны данные карты и таблиц.

Требования:

  • generated_at не использовать как дату актуальности данных;
  • data_actual_at должен быть единым для snapshot-а карты и таблицы организаций при одинаковых фильтрах;
  • если разные блоки приходят из источников с разной актуальностью, backend должен либо привести их к единому согласованному snapshot-у, либо явно согласовать и задокументировать методику;
  • версия методики расчета уникальности организаций, участий и принадлежности к регионам должна быть зафиксирована в backend-документации;
  • если backend поддерживает версионирование методик, response может дополнительно возвращать calculation_methodology_version;
  • изменение методики, которое меняет смысл существующих полей, считается breaking change для /api/v1.

Большие справочники и массивы

Dashboard-массивы map.regions, map.federal_districts, association_list.rows и region_ranking.rows возвращаются полностью.

Табличный перечень организаций может быть большим, поэтому он вынесен в отдельный endpoint /association-map/organizations/ с limit и offset.

Требования к таблице организаций:

  • limit default 500;
  • limit maximum 1000;
  • offset default 0;
  • total_count всегда возвращается независимо от limit;
  • unique_organizations_count возвращается отдельно от количества строк, потому что одна организация может иметь несколько строк участия;
  • если текущий UI хочет показать таблицу без пагинации, frontend может запросить limit=1000;
  • если total_count > limit, frontend должен догружать следующие страницы или включить пагинацию/виртуализацию.

Для справочника organizations в /association-map/filters/ также возможен большой объем данных. Если список может превышать безопасный размер для одного response, backend должен выбрать один из вариантов до реализации:

  • возвращать только доступные организации в пределах текущих фильтров, если объем гарантированно небольшой;
  • добавить к /association-map/filters/ query params organization_search и organization_limit;
  • вынести поиск организаций в отдельный endpoint справочника.

Если вводятся organization_search и organization_limit, правила должны быть такими:

  • organization_search: string, optional, минимум 2 символа после trim;
  • organization_limit: integer, optional, default 50, maximum 100;
  • сортировка организаций: релевантность поиска, затем label по возрастанию (ru-RU);
  • выбранная в organization_id организация должна присутствовать в organizations, даже если она не попала бы в первые organization_limit результатов.

Термины и правила подсчета

association_type

Тип инновационного объединения для карты.

Допустимые значения:

  • clusters: промышленные кластеры;
  • technoparks: технопарки;
  • sez: особые экономические зоны.

Для страниц карты значение all не используется. Объединенный режим всех типов относится к аналитической панели /dashboard и описан в отдельном документе.

Соответствие frontend routes:

Route association_type
/new-map clusters
/new-map/technoparks technoparks
/new-map/sez sez

map_view_mode

Тип отображения карты.

Допустимые значения:

  • regions: карта субъектов РФ;
  • federal_districts: карта федеральных округов.

map_view_mode сам по себе не меняет методику подсчета данных. Он определяет активное визуальное представление и то, какие id frontend будет передавать при клике по карте.

region_id

Идентификатор субъекта РФ для карты.

Требования:

  • должен совпадать с SVG-id субъекта РФ в компоненте карты;
  • формат: RU-*;
  • примеры: RU-MOS, RU-MOW, RU-TA, RU-CHE;
  • если у backend-а есть внутренний id региона, backend должен уметь сопоставить его с map_region_id.

federal_district_id

Идентификатор федерального округа.

Требования:

  • стабильный string id;
  • используется в фильтрах, карте федеральных округов и связке субъектов РФ с ФО;
  • рекомендуемые значения: cfo, nwfo, yufo, pfo, ufo, sfo, dfo, skfo.

region_ids и federal_district_ids

Множественный выбор на карте.

Назначение:

  • region_ids используется, когда пользователь выбрал один или несколько субъектов РФ на карте;
  • federal_district_ids используется, когда пользователь выбрал один или несколько федеральных округов;
  • оба массива работают как фильтр данных страницы.

Правила:

  • массивы могут быть пустыми;
  • пустой массив означает, что multi-selection на карте не применен;
  • если передан массив, соответствующий одиночный select-фильтр должен быть пустым;
  • region_ids и federal_district_ids взаимоисключающие;
  • порядок id в applied_filters должен сохранять порядок, нормализованный backend-ом.

associations_count

Количество инновационных объединений выбранного association_type после применения фильтров.

Примеры:

  • для clusters это количество промышленных кластеров;
  • для technoparks это количество технопарков;
  • для sez это количество ОЭЗ.

opk_organizations_count

Количество уникальных организаций ОПК после применения фильтров.

Одна организация ОПК может входить в несколько объединений. Поэтому это поле не должно считаться как простая сумма строк таблицы организаций, если таблица возвращает строки участия.

opk_memberships_count

Количество связей организация ОПК x инновационное объединение.

Это поле может быть больше opk_organizations_count. Если таблица организаций возвращает строки участия, total_count таблицы может совпадать с opk_memberships_count, но не обязан совпадать с opk_organizations_count.

organization_directory.total_count

Количество строк в табличном перечне организаций после применения фильтров и до применения limit/offset.

Требования:

  • значение используется для UI-счетчика Найдено: N;
  • если таблица возвращает строки участия, а не уникальные организации, значение может быть больше unique_organizations_count;
  • семантика строки таблицы должна быть зафиксирована в поле row_granularity.

rank

Позиция строки в рейтинге.

Требования:

  • rank возвращается backend-ом как integer >= 1;
  • если используется обычное порядковое ранжирование, rank не должен дублироваться;
  • если бизнес-методика допускает одинаковые места при равных значениях, блок должен вернуть ranking_method;
  • допустимые значения ranking_method: ordinal, dense, competition;
  • если ranking_method не передан, frontend считает методику ordinal.

Полнота массивов

Если в блоке нет явного limit, массив rows, regions или federal_districts должен возвращаться полностью для текущего состояния страницы и текущих фильтров.

Frontend может отображать часть данных в видимой области и скроллить контейнер локально, но backend не должен неявно обрезать массив до первых 5-10 строк только потому, что в макете виден короткий пример.


Нужно реализовать

competence-map association-map filters

GET /api/v1/competence-map/association-map/filters/

Описание:

  • возвращает справочники для фильтров страниц карты;
  • учитывает права пользователя;
  • учитывает текущие выбранные фильтры, если они переданы в query params;
  • возвращает серверно доступные значения association_type;
  • возвращает доступные типы отображения карты;
  • не возвращает KPI, рейтинг, список объединений и таблицу организаций;
  • не управляет визуальным представлением фильтров: frontend может локально показывать типы объединений как route navigation, tabs или segmented control.

Request:

GET /api/v1/competence-map/association-map/filters/?association_type=clusters

Query params:

  • association_type: clusters | technoparks | sez, optional, default clusters;
  • map_view_mode: regions | federal_districts, optional, default regions;
  • federal_district_id: string, optional;
  • region_id: string, optional;
  • federal_district_ids: array of string, optional;
  • region_ids: array of string, optional;
  • industry_id: string, optional;
  • integrated_structure_id: string, optional;
  • organization_id: string, optional;
  • competence_id: string, optional.

Response:

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "applied_filters": {
    "association_type": "clusters",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "association_types": [
    {
      "id": "clusters",
      "label": "Кластеры",
      "route_path": "/new-map",
      "associations_count": 124,
      "disabled": false
    },
    {
      "id": "technoparks",
      "label": "Технопарки",
      "route_path": "/new-map/technoparks",
      "associations_count": 35,
      "disabled": false
    },
    {
      "id": "sez",
      "label": "Особые экономические зоны",
      "route_path": "/new-map/sez",
      "associations_count": 17,
      "disabled": false
    }
  ],
  "map_view_modes": [
    {
      "id": "regions",
      "label": "Субъекты РФ",
      "disabled": false
    },
    {
      "id": "federal_districts",
      "label": "Федеральные округа",
      "disabled": false
    }
  ],
  "federal_districts": [
    {
      "id": "cfo",
      "label": "Центральный ФО",
      "region_ids": ["RU-BEL", "RU-MOS", "RU-MOW", "RU-YAR"],
      "associations_count": 58,
      "opk_organizations_count": 163,
      "disabled": false
    }
  ],
  "regions": [
    {
      "id": "RU-MOS",
      "map_region_id": "RU-MOS",
      "federal_district_id": "cfo",
      "label": "Московская область",
      "associations_count": 17,
      "opk_organizations_count": 68,
      "disabled": false
    }
  ],
  "industries": [
    {
      "id": "radio-electronics",
      "label": "Радиоэлектронная промышленность",
      "opk_organizations_count": 180,
      "disabled": false
    }
  ],
  "integrated_structures": [
    {
      "id": "rostec",
      "label": "Госкорпорация Ростех",
      "opk_organizations_count": 42,
      "disabled": false
    }
  ],
  "organizations": [
    {
      "id": "org-concern-agat",
      "label": "АО \"Концерн \"Моринсис - Агат\"",
      "inn": "7700000000",
      "ogrn": "1027700000000",
      "association_memberships_count": 1,
      "disabled": false
    }
  ],
  "competences": [
    {
      "id": "microelectronics",
      "label": "Микроэлектроника",
      "opk_organizations_count": 64,
      "disabled": false
    }
  ]
}

Требования к association-map/filters/

Поле generated_at

Требования:

  • ISO 8601 с timezone;
  • обязательное поле;
  • единое время генерации для всего payload.

Блок applied_filters

Требования:

  • возвращает фактически примененные фильтры;
  • все отсутствующие одиночные значения возвращаются как null;
  • все отсутствующие multi-selection значения возвращаются как [];
  • association_type всегда заполнен;
  • map_view_mode всегда заполнен.

Массив association_types

Требования:

  • содержит все доступные пользователю типы объединений для страниц карты;
  • id должен быть одним из clusters, technoparks, sez;
  • label обязательный;
  • route_path optional, но если возвращается, должен соответствовать frontend route;
  • associations_count integer >= 0;
  • disabled boolean.

Назначение:

  • является серверным источником доступных разделов карты;
  • порядок массива определяет рекомендуемый порядок навигации;
  • при выборе раздела frontend передает выбранный id как query param association_type;
  • associations_count может использоваться как серверное значение для badge/count рядом с разделом;
  • disabled = true означает, что тип недоступен пользователю или неприменим при текущих фильтрах.

Массив map_view_modes

Требования:

  • содержит regions и federal_districts, если оба режима доступны пользователю;
  • id обязательный;
  • label обязательный;
  • disabled обязательный.

Массивы справочников

Для массивов federal_districts, regions, industries, integrated_structures, organizations, competences:

  • id обязательный;
  • label обязательный;
  • disabled обязательный;
  • count-поля должны быть integer >= 0;
  • если по текущим фильтрам список пустой, возвращается пустой массив;
  • выбранное значение должно присутствовать в справочнике, если пользователь имеет к нему доступ.

Для regions дополнительно:

  • map_region_id обязательный;
  • federal_district_id обязательный;
  • map_region_id должен совпадать с id региона в SVG-карте субъектов РФ.

Для federal_districts дополнительно:

  • region_ids обязательный;
  • каждый region_ids[*] должен существовать в справочнике regions, если регион доступен пользователю.

competence-map association-map

GET /api/v1/competence-map/association-map/

Описание:

  • возвращает основную аналитическую часть страницы карты для выбранного association_type;
  • применяет все переданные фильтры;
  • применяет выбранные на карте субъекты РФ или федеральные округа как фильтр;
  • возвращает KPI, данные карты, список объединений, рейтинг субъектов РФ и summary таблицы организаций;
  • не возвращает полный табличный перечень организаций, для него используется отдельный endpoint /association-map/organizations/.

Request:

GET /api/v1/competence-map/association-map/?association_type=clusters

Query params:

  • association_type: clusters | technoparks | sez, optional, default clusters;
  • map_view_mode: regions | federal_districts, optional, default regions;
  • federal_district_id: string, optional;
  • region_id: string, optional;
  • federal_district_ids: array of string, optional;
  • region_ids: array of string, optional;
  • industry_id: string, optional;
  • integrated_structure_id: string, optional;
  • organization_id: string, optional;
  • competence_id: string, optional.

Общий response envelope:

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "clusters",
  "applied_filters": {
    "association_type": "clusters",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "page": {},
  "kpis": [],
  "map": {},
  "association_list": {},
  "region_ranking": {},
  "organization_directory": {}
}

Серверные состояния страницы по association_type

Страница имеет три серверных состояния. Они используют один и тот же endpoint /api/v1/competence-map/association-map/.

Frontend route association_type Primary KPI Список объединений Рейтинг
/new-map clusters clusters_count промышленные кластеры субъекты РФ по участию организаций ОПК в кластерах
/new-map/technoparks technoparks technoparks_count технопарки субъекты РФ по участию организаций ОПК в технопарках
/new-map/sez sez sez_count ОЭЗ субъекты РФ по участию организаций ОПК в ОЭЗ

Требования:

  • association_type в корне response должен совпадать с applied_filters.association_type;
  • frontend не должен делать отдельные endpoint-ы /clusters, /technoparks, /sez;
  • при переходе между frontend routes меняется только association_type и повторно запрашиваются /association-map/filters/ и /association-map/;
  • map_view_mode меняет активное представление карты, но не требует отдельного backend resource.

Response для association_type=clusters

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "clusters",
  "applied_filters": {
    "association_type": "clusters",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "page": {
    "title": "Участие организаций ОПК в кластерах",
    "subtitle": "Российская Федерация",
    "legend_label": "Присутствие одной или более организаций ОПК в кластере",
    "empty_list_label": "По текущим фильтрам кластеры не найдены."
  },
  "kpis": [
    {
      "code": "clusters_count",
      "label": "Количество кластеров",
      "value": 124,
      "value_type": "integer",
      "numerator": 124,
      "denominator": null
    },
    {
      "code": "opk_organizations_count",
      "label": "Количество организаций ОПК",
      "value": 517,
      "value_type": "integer",
      "numerator": 517,
      "denominator": null
    }
  ],
  "map": {
    "view_mode": "regions",
    "selected_scope": null,
    "selected_region_ids": [],
    "selected_federal_district_ids": [],
    "selected_chip_text": null,
    "highlight_color": "#022456",
    "regions": [
      {
        "region_id": "RU-MOS",
        "map_region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "associations_count": 17,
        "opk_organizations_count": 68,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Московская область: 17 кластеров, 68 организаций ОПК"
      },
      {
        "region_id": "RU-MOW",
        "map_region_id": "RU-MOW",
        "region_name": "Москва",
        "federal_district_id": "cfo",
        "associations_count": 11,
        "opk_organizations_count": 34,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Москва: 11 кластеров, 34 организации ОПК"
      }
    ],
    "federal_districts": [
      {
        "federal_district_id": "cfo",
        "federal_district_name": "Центральный ФО",
        "region_ids": ["RU-MOS", "RU-MOW", "RU-YAR", "RU-BEL"],
        "associations_count": 37,
        "opk_organizations_count": 143,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Центральный ФО: 37 кластеров, 143 организации ОПК"
      }
    ]
  },
  "association_list": {
    "title": "Кластеры, в числе участников которых есть организации ОПК",
    "row_metric_label": "Количество организаций ОПК",
    "total_count": 124,
    "rows": [
      {
        "association_id": "cluster-ru-mos-001",
        "association_type": "clusters",
        "association_name": "Радиоэлектронный кластер Подмосковья",
        "region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "opk_organizations_count": 9,
        "opk_memberships_count": 9
      }
    ]
  },
  "region_ranking": {
    "title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах",
    "sort": {
      "field": "associations_count",
      "direction": "desc"
    },
    "ranking_method": "ordinal",
    "columns": [
      {
        "field": "associations_count",
        "label": "Количество кластеров",
        "value_type": "integer"
      },
      {
        "field": "opk_organizations_count",
        "label": "Количество организаций ОПК",
        "value_type": "integer"
      }
    ],
    "rows": [
      {
        "rank": 1,
        "region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "associations_count": 17,
        "opk_organizations_count": 68,
        "opk_memberships_count": 68
      },
      {
        "rank": 2,
        "region_id": "RU-MOW",
        "region_name": "Москва",
        "federal_district_id": "cfo",
        "associations_count": 11,
        "opk_organizations_count": 34,
        "opk_memberships_count": 34
      }
    ]
  },
  "organization_directory": {
    "title": "Перечень организаций ОПК",
    "endpoint": "/api/v1/competence-map/association-map/organizations/",
    "row_granularity": "organization_association_membership",
    "total_count": 517,
    "unique_organizations_count": 517
  }
}

Response для association_type=technoparks

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "technoparks",
  "applied_filters": {
    "association_type": "technoparks",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "page": {
    "title": "Участие организаций ОПК в технопарках",
    "subtitle": "Российская Федерация",
    "legend_label": "Присутствие одной или более организаций ОПК в технопарке",
    "empty_list_label": "По текущим фильтрам технопарки не найдены."
  },
  "kpis": [
    {
      "code": "technoparks_count",
      "label": "Количество технопарков",
      "value": 35,
      "value_type": "integer",
      "numerator": 35,
      "denominator": null
    },
    {
      "code": "opk_organizations_count",
      "label": "Количество организаций ОПК",
      "value": 66,
      "value_type": "integer",
      "numerator": 66,
      "denominator": null
    }
  ],
  "map": {
    "view_mode": "regions",
    "selected_scope": null,
    "selected_region_ids": [],
    "selected_federal_district_ids": [],
    "selected_chip_text": null,
    "highlight_color": "#022456",
    "regions": [
      {
        "region_id": "RU-MOS",
        "map_region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "associations_count": 6,
        "opk_organizations_count": 12,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Московская область: 6 технопарков, 12 организаций ОПК"
      }
    ],
    "federal_districts": [
      {
        "federal_district_id": "cfo",
        "federal_district_name": "Центральный ФО",
        "region_ids": ["RU-MOS", "RU-MOW", "RU-YAR", "RU-BEL"],
        "associations_count": 14,
        "opk_organizations_count": 28,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Центральный ФО: 14 технопарков, 28 организаций ОПК"
      }
    ]
  },
  "association_list": {
    "title": "Технопарки, в числе участников которых есть организации ОПК",
    "row_metric_label": "Количество организаций ОПК",
    "total_count": 35,
    "rows": [
      {
        "association_id": "technopark-ru-mos-001",
        "association_type": "technoparks",
        "association_name": "Подмосковный технопарк высоких технологий",
        "region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "opk_organizations_count": 4,
        "opk_memberships_count": 4
      }
    ]
  },
  "region_ranking": {
    "title": "Рейтинг субъектов РФ по участию организаций ОПК в технопарках",
    "sort": {
      "field": "associations_count",
      "direction": "desc"
    },
    "ranking_method": "ordinal",
    "columns": [
      {
        "field": "associations_count",
        "label": "Количество технопарков",
        "value_type": "integer"
      },
      {
        "field": "opk_organizations_count",
        "label": "Количество организаций ОПК",
        "value_type": "integer"
      }
    ],
    "rows": [
      {
        "rank": 1,
        "region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo",
        "associations_count": 6,
        "opk_organizations_count": 12,
        "opk_memberships_count": 12
      }
    ]
  },
  "organization_directory": {
    "title": "Перечень организаций ОПК",
    "endpoint": "/api/v1/competence-map/association-map/organizations/",
    "row_granularity": "organization_association_membership",
    "total_count": 66,
    "unique_organizations_count": 66
  }
}

Response для association_type=sez

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "sez",
  "applied_filters": {
    "association_type": "sez",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "page": {
    "title": "Участие организаций ОПК в особых экономических зонах",
    "subtitle": "Российская Федерация",
    "legend_label": "Присутствие одной или более организаций ОПК в особой экономической зоне",
    "empty_list_label": "По текущим фильтрам особые экономические зоны не найдены."
  },
  "kpis": [
    {
      "code": "sez_count",
      "label": "Количество ОЭЗ",
      "value": 17,
      "value_type": "integer",
      "numerator": 17,
      "denominator": null
    },
    {
      "code": "opk_organizations_count",
      "label": "Количество организаций ОПК",
      "value": 64,
      "value_type": "integer",
      "numerator": 64,
      "denominator": null
    }
  ],
  "map": {
    "view_mode": "regions",
    "selected_scope": null,
    "selected_region_ids": [],
    "selected_federal_district_ids": [],
    "selected_chip_text": null,
    "highlight_color": "#022456",
    "regions": [
      {
        "region_id": "RU-KGN",
        "map_region_id": "RU-KGN",
        "region_name": "Курганская область",
        "federal_district_id": "ufo",
        "associations_count": 9,
        "opk_organizations_count": 9,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Курганская область: 9 ОЭЗ, 9 организаций ОПК"
      }
    ],
    "federal_districts": [
      {
        "federal_district_id": "ufo",
        "federal_district_name": "Уральский ФО",
        "region_ids": ["RU-CHE", "RU-KGN", "RU-SVE"],
        "associations_count": 15,
        "opk_organizations_count": 21,
        "has_data": true,
        "is_highlighted": false,
        "tooltip_label": "Уральский ФО: 15 ОЭЗ, 21 организация ОПК"
      }
    ]
  },
  "association_list": {
    "title": "Особые экономические зоны, в числе участников которых есть организации ОПК",
    "row_metric_label": "Количество организаций ОПК",
    "total_count": 17,
    "rows": [
      {
        "association_id": "sez-kurgan-4",
        "association_type": "sez",
        "association_name": "Особая экономическая зона \"Курганский 4\"",
        "region_id": "RU-KGN",
        "region_name": "Курганская область",
        "federal_district_id": "ufo",
        "opk_organizations_count": 9,
        "opk_memberships_count": 9
      }
    ]
  },
  "region_ranking": {
    "title": "Рейтинг субъектов РФ по участию организаций ОПК в особых экономических зонах",
    "sort": {
      "field": "associations_count",
      "direction": "desc"
    },
    "ranking_method": "ordinal",
    "columns": [
      {
        "field": "associations_count",
        "label": "Количество ОЭЗ",
        "value_type": "integer"
      },
      {
        "field": "opk_organizations_count",
        "label": "Количество организаций ОПК",
        "value_type": "integer"
      }
    ],
    "rows": [
      {
        "rank": 1,
        "region_id": "RU-KGN",
        "region_name": "Курганская область",
        "federal_district_id": "ufo",
        "associations_count": 9,
        "opk_organizations_count": 9,
        "opk_memberships_count": 9
      }
    ]
  },
  "organization_directory": {
    "title": "Перечень организаций ОПК",
    "endpoint": "/api/v1/competence-map/association-map/organizations/",
    "row_granularity": "organization_association_membership",
    "total_count": 64,
    "unique_organizations_count": 64
  }
}

competence-map association-map organizations

GET /api/v1/competence-map/association-map/organizations/

Описание:

  • возвращает табличный перечень организаций ОПК для выбранного состояния карты;
  • применяет те же фильтры, что /association-map/;
  • используется для экрана, который открывается по ссылке Перейти к перечню организаций;
  • возвращает строки таблицы, счетчик Найдено, pagination metadata и нормализованные примененные фильтры.

Request:

GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&region_ids=RU-MOS&limit=500&offset=0

Query params:

  • association_type: clusters | technoparks | sez, optional, default clusters;
  • map_view_mode: regions | federal_districts, optional, default regions;
  • federal_district_id: string, optional;
  • region_id: string, optional;
  • federal_district_ids: array of string, optional;
  • region_ids: array of string, optional;
  • industry_id: string, optional;
  • integrated_structure_id: string, optional;
  • organization_id: string, optional;
  • competence_id: string, optional;
  • limit: integer, optional, default 500, maximum 1000;
  • offset: integer, optional, default 0;
  • sort_field: string, optional, default serial_number;
  • sort_direction: asc | desc, optional, default asc.

Response:

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "clusters",
  "applied_filters": {
    "association_type": "clusters",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": null,
    "federal_district_ids": [],
    "region_ids": ["RU-MOS"],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "row_granularity": "organization_association_membership",
  "total_count": 68,
  "unique_organizations_count": 68,
  "pagination": {
    "limit": 500,
    "offset": 0,
    "has_next": false,
    "has_previous": false
  },
  "sort": {
    "field": "serial_number",
    "direction": "asc"
  },
  "columns": [
    {
      "field": "serial_number",
      "label": "№ п/п",
      "value_type": "integer"
    },
    {
      "field": "organization_name",
      "label": "Наименование",
      "value_type": "string"
    },
    {
      "field": "director_full_name",
      "label": "ФИО руководителя",
      "value_type": "string"
    },
    {
      "field": "official_emails",
      "label": "Официальная почта",
      "value_type": "string_array"
    },
    {
      "field": "official_phones",
      "label": "Официальный телефон",
      "value_type": "string_array"
    },
    {
      "field": "industry_name",
      "label": "Отрасль",
      "value_type": "string"
    },
    {
      "field": "activity_type_name",
      "label": "Вид деятельности",
      "value_type": "string"
    },
    {
      "field": "ownership_type_name",
      "label": "Тип собственности",
      "value_type": "string"
    }
  ],
  "rows": [
    {
      "row_id": "cluster-ru-mos-001:org-concern-agat",
      "serial_number": 1,
      "organization_id": "org-concern-agat",
      "organization_name": "АО \"Концерн \"Моринсис - Агат\"",
      "director_full_name": "Храмов М.Ю.",
      "official_emails": ["info@concern-agat.ru", "gen_director@concern-agat.ru"],
      "official_phones": ["603-90-05"],
      "industry_id": "radio-electronics",
      "industry_name": "Радиоэлектронная промышленность",
      "activity_type_id": "research-organizations",
      "activity_type_name": "Научные организации",
      "ownership_type_id": "directly-or-indirectly-controlled",
      "ownership_type_name": "Прямо или косвенно контролируемые",
      "association_membership": {
        "association_type": "clusters",
        "association_id": "cluster-ru-mos-001",
        "association_name": "Радиоэлектронный кластер Подмосковья",
        "region_id": "RU-MOS",
        "region_name": "Московская область",
        "federal_district_id": "cfo"
      }
    }
  ]
}

Требования к association-map/organizations/

Поле row_granularity

Допустимые значения:

  • unique_organization;
  • organization_association_membership.

Для текущего табличного представления рекомендуется organization_association_membership, потому что одна организация может участвовать в нескольких объединениях или отображаться в разных отраслевых контекстах.

Требования:

  • если row_granularity = unique_organization, total_count должен быть равен unique_organizations_count;
  • если row_granularity = organization_association_membership, total_count может быть больше unique_organizations_count;
  • row_id должен быть стабильным id строки, а не только organization_id.

Поля организации

Требования:

  • organization_id обязательный;
  • organization_name обязательный;
  • director_full_name может быть null, если данных нет;
  • official_emails возвращается массивом строк, может быть пустым массивом;
  • official_phones возвращается массивом строк, может быть пустым массивом;
  • industry_id и industry_name обязательны, если строка имеет отраслевой контекст;
  • activity_type_id и activity_type_name обязательны, если источник данных содержит вид деятельности;
  • ownership_type_id и ownership_type_name обязательны, если источник данных содержит тип собственности;
  • неизвестные значения возвращаются как null или пустой массив, а не как строка -.

Блок association_membership

Требования:

  • обязателен, если row_granularity = organization_association_membership;
  • association_type должен совпадать с корневым association_type;
  • association_id обязательный;
  • association_name обязательный;
  • region_id должен быть совместим с SVG-картой;
  • federal_district_id обязательный.

Требования к общим полям association-map response

Поле generated_at

Требования:

  • ISO 8601 с timezone;
  • обязательное;
  • показывает время формирования response.

Поле data_actual_at

Требования:

  • ISO 8601 с timezone;
  • обязательное для /association-map/ и /association-map/organizations/;
  • показывает дату и время актуальности данных.

Поле association_type

Требования:

  • обязательное;
  • должно совпадать с фактически примененным режимом;
  • допустимые значения: clusters, technoparks, sez.

Блок applied_filters

Требования:

  • обязательный;
  • содержит нормализованные значения примененных фильтров;
  • отсутствующие одиночные фильтры возвращаются как null;
  • отсутствующие multi-selection фильтры возвращаются как [];
  • association_type и map_view_mode всегда заполнены.

Блок page

Поля:

  • title;
  • subtitle;
  • legend_label;
  • empty_list_label.

Требования:

  • все поля обязательны для /association-map/;
  • значения должны соответствовать текущему association_type;
  • frontend может использовать эти строки напрямую или локализовать их на своей стороне.

Массив kpis

Каждый объект массива - одна KPI-метрика.

Поля:

  • code: стабильный системный идентификатор метрики;
  • label: человекочитаемое название метрики;
  • value: raw numeric value;
  • value_type: integer | percent | decimal;
  • numerator: числитель, если применимо;
  • denominator: знаменатель, если применимо.

Требования:

  • все поля, кроме numerator и denominator, обязательны;
  • value должен быть number, не string;
  • 0 допустим как валидное значение;
  • для value_type = percent значение возвращается в диапазоне 0..100;
  • порядок массива должен соответствовать порядку карточек на странице;
  • для текущих страниц карты порядок: count выбранного типа объединения, затем количество организаций ОПК.

Блок map

Поля:

  • view_mode;
  • selected_scope;
  • selected_region_ids;
  • selected_federal_district_ids;
  • selected_chip_text;
  • highlight_color;
  • regions;
  • federal_districts.

Требования:

  • view_mode должен совпадать с applied_filters.map_view_mode;
  • selected_scope должен быть regions, federal_districts или null;
  • если применен region_id или region_ids, selected_scope = regions;
  • если применен federal_district_id или federal_district_ids, selected_scope = federal_districts;
  • selected_region_ids и selected_federal_district_ids обязательны, даже если пустые;
  • selected ids описывают эффективный выбор географии, включая одиночные select-фильтры region_id/federal_district_id и multi-selection фильтры region_ids/federal_district_ids;
  • selected_chip_text может быть null, если ничего не выбрано;
  • highlight_color optional, но если возвращается, должен быть CSS-compatible color string;
  • regions должен включать все доступные пользователю субъекты РФ, а не только подсвеченные;
  • federal_districts должен включать все доступные пользователю федеральные округа, а не только подсвеченные.

map.regions

Каждый объект массива - один субъект РФ.

Поля:

  • region_id;
  • map_region_id;
  • region_name;
  • federal_district_id;
  • associations_count;
  • opk_organizations_count;
  • has_data;
  • is_highlighted;
  • tooltip_label.

Требования:

  • region_id обязательный;
  • map_region_id обязательный;
  • map_region_id должен совпадать с SVG-id региона;
  • associations_count integer >= 0;
  • opk_organizations_count integer >= 0;
  • has_data = true, если по текущим фильтрам в регионе есть хотя бы одно объединение выбранного типа или хотя бы одна организация ОПК;
  • is_highlighted = true, если регион должен быть подсвечен при текущих фильтрах/выборе;
  • tooltip_label optional, frontend может построить подпись локально из raw-полей.

map.federal_districts

Каждый объект массива - один федеральный округ.

Поля:

  • federal_district_id;
  • federal_district_name;
  • region_ids;
  • associations_count;
  • opk_organizations_count;
  • has_data;
  • is_highlighted;
  • tooltip_label.

Требования:

  • federal_district_id обязательный;
  • region_ids обязательный;
  • region_ids должен содержать id субъектов РФ, входящих в ФО и доступных пользователю;
  • count-поля должны быть агрегированы по регионам округа с учетом текущих фильтров;
  • is_highlighted = true, если округ должен быть подсвечен при текущих фильтрах/выборе.

Блок association_list

Поля:

  • title;
  • row_metric_label;
  • total_count;
  • rows.

Требования:

  • title должен соответствовать текущему association_type;
  • row_metric_label для текущего UI: Количество организаций ОПК;
  • total_count integer >= 0;
  • rows возвращаются полностью для текущих фильтров;
  • строки должны быть отсортированы по opk_organizations_count по убыванию, затем association_name по возрастанию (ru-RU), если не задан другой backend-порядок.

Каждая строка:

  • association_id обязательный;
  • association_type обязательный;
  • association_name обязательный;
  • region_id обязательный;
  • region_name обязательный;
  • federal_district_id обязательный;
  • opk_organizations_count integer >= 0;
  • opk_memberships_count integer >= 0.

Блок region_ranking

Поля:

  • title;
  • sort;
  • ranking_method;
  • columns;
  • rows.

Требования:

  • title обязательный;
  • sort.field обязательный;
  • sort.direction должен быть asc | desc;
  • ranking_method optional, default ordinal;
  • columns обязательный, чтобы frontend не хардкодил Количество кластеров для страниц ОЭЗ/технопарков;
  • rows может быть пустым массивом;
  • rank возвращается в каждой строке;
  • region_id обязательный;
  • region_name обязательный;
  • associations_count integer >= 0;
  • opk_organizations_count integer >= 0.

Блок organization_directory

Поля:

  • title;
  • endpoint;
  • row_granularity;
  • total_count;
  • unique_organizations_count.

Требования:

  • используется как summary для кнопки/ссылки перехода к таблице организаций;
  • endpoint должен указывать на /api/v1/competence-map/association-map/organizations/;
  • total_count должен совпадать с total_count, который вернет organizations endpoint при тех же фильтрах;
  • unique_organizations_count должен совпадать с unique_organizations_count, который вернет organizations endpoint при тех же фильтрах.

Порядок массивов

association_types

Рекомендуемый порядок:

  1. clusters
  2. technoparks
  3. sez

map_view_modes

Рекомендуемый порядок:

  1. regions
  2. federal_districts

map.regions

Порядок сортировки:

  1. federal_district_id в порядке справочника федеральных округов;
  2. region_name по возрастанию (ru-RU).

map.federal_districts

Порядок сортировки должен соответствовать порядку федеральных округов на backend-е. Если такого порядка нет, использовать:

  1. cfo
  2. nwfo
  3. yufo
  4. skfo
  5. pfo
  6. ufo
  7. sfo
  8. dfo

association_list.rows

Порядок сортировки:

  1. opk_organizations_count по убыванию;
  2. association_name по возрастанию (ru-RU);
  3. association_id по возрастанию.

region_ranking.rows

Порядок сортировки по умолчанию:

  1. associations_count по убыванию;
  2. при равенстве - opk_organizations_count по убыванию;
  3. при равенстве - region_name по возрастанию (ru-RU).

Если в блоке передан ranking_method = dense или competition, одинаковые rank допустимы только для строк с равным основным показателем.

organizations.rows

Порядок сортировки по умолчанию:

  1. serial_number по возрастанию.

serial_number должен соответствовать позиции строки после применения фильтров и сортировки, а не позиции внутри текущей страницы limit/offset.


Проверки консистентности

Backend должен гарантировать:

  • association_type в корне response совпадает с applied_filters.association_type;
  • map.view_mode совпадает с applied_filters.map_view_mode;
  • kpis[*].value согласованы с соответствующими блоками карты и таблицы;
  • primary KPI count выбранного типа объединения равен association_list.total_count;
  • organization_directory.total_count равен total_count organizations endpoint-а при тех же фильтрах;
  • organization_directory.unique_organizations_count равен unique_organizations_count organizations endpoint-а при тех же фильтрах;
  • map.regions[*].region_id существует в справочнике regions;
  • map.federal_districts[*].region_ids ссылаются на существующие map.regions[*].region_id;
  • region_ranking.rows[*].region_id существует в map.regions;
  • если selected_scope = regions, selected_region_ids не пустой и selected_federal_district_ids пустой;
  • если selected_scope = federal_districts, selected_federal_district_ids не пустой и selected_region_ids пустой;
  • если selected_scope = null, оба массива selected ids пустые;
  • rank в рейтингах не дублируется, если ranking_method = ordinal;
  • все id стабильны между запросами;
  • все count-поля возвращаются как integer >= 0;
  • пустые данные возвращаются пустыми массивами, а не отсутствующими полями.

Успешный пустой ответ

Если по фильтрам данных нет, возвращается 200 OK.

Пример:

{
  "generated_at": "2026-06-24T15:20:00+03:00",
  "data_actual_at": "2026-06-24T00:00:00+03:00",
  "association_type": "clusters",
  "applied_filters": {
    "association_type": "clusters",
    "map_view_mode": "regions",
    "federal_district_id": null,
    "region_id": "RU-NEN",
    "federal_district_ids": [],
    "region_ids": [],
    "industry_id": null,
    "integrated_structure_id": null,
    "organization_id": null,
    "competence_id": null
  },
  "page": {
    "title": "Участие организаций ОПК в кластерах",
    "subtitle": "Российская Федерация",
    "legend_label": "Присутствие одной или более организаций ОПК в кластере",
    "empty_list_label": "По текущим фильтрам кластеры не найдены."
  },
  "kpis": [
    {
      "code": "clusters_count",
      "label": "Количество кластеров",
      "value": 0,
      "value_type": "integer",
      "numerator": 0,
      "denominator": null
    },
    {
      "code": "opk_organizations_count",
      "label": "Количество организаций ОПК",
      "value": 0,
      "value_type": "integer",
      "numerator": 0,
      "denominator": null
    }
  ],
  "map": {
    "view_mode": "regions",
    "selected_scope": "regions",
    "selected_region_ids": ["RU-NEN"],
    "selected_federal_district_ids": [],
    "selected_chip_text": "Ненецкий автономный округ: 0 кластеров",
    "highlight_color": "#022456",
    "regions": [
      {
        "region_id": "RU-NEN",
        "map_region_id": "RU-NEN",
        "region_name": "Ненецкий автономный округ",
        "federal_district_id": "nwfo",
        "associations_count": 0,
        "opk_organizations_count": 0,
        "has_data": false,
        "is_highlighted": false,
        "tooltip_label": "Ненецкий автономный округ: данных по текущим фильтрам нет"
      }
    ],
    "federal_districts": [
      {
        "federal_district_id": "nwfo",
        "federal_district_name": "Северо-Западный ФО",
        "region_ids": ["RU-NEN"],
        "associations_count": 0,
        "opk_organizations_count": 0,
        "has_data": false,
        "is_highlighted": false,
        "tooltip_label": "Северо-Западный ФО: данных по текущим фильтрам нет"
      }
    ]
  },
  "association_list": {
    "title": "Кластеры, в числе участников которых есть организации ОПК",
    "row_metric_label": "Количество организаций ОПК",
    "total_count": 0,
    "rows": []
  },
  "region_ranking": {
    "title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах",
    "sort": {
      "field": "associations_count",
      "direction": "desc"
    },
    "ranking_method": "ordinal",
    "columns": [
      {
        "field": "associations_count",
        "label": "Количество кластеров",
        "value_type": "integer"
      },
      {
        "field": "opk_organizations_count",
        "label": "Количество организаций ОПК",
        "value_type": "integer"
      }
    ],
    "rows": []
  },
  "organization_directory": {
    "title": "Перечень организаций ОПК",
    "endpoint": "/api/v1/competence-map/association-map/organizations/",
    "row_granularity": "organization_association_membership",
    "total_count": 0,
    "unique_organizations_count": 0
  }
}

Статус-коды и ошибки

Status code matrix

Status Когда возвращается
200 OK Успешный ответ, включая валидные фильтры без данных.
304 Not Modified Conditional GET по If-None-Match или If-Modified-Since, если представление не изменилось.
400 Bad Request Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения, конфликт фильтров.
401 Unauthorized Пользователь не аутентифицирован.
403 Forbidden Пользователь аутентифицирован, но не имеет доступа к странице или разделу.
429 Too Many Requests Превышен rate limit.
500 Internal Server Error Непредвиденная ошибка backend-а.
503 Service Unavailable Временная недоступность сервиса или источника данных карты.

Для collection/report endpoint-ов не использовать 404 Not Found, если path существует, но по валидным фильтрам нет данных. В этом случае нужен 200 OK с пустыми массивами.

Problem Details

Все ошибки возвращаются в формате application/problem+json.

Обязательные поля:

  • type: стабильный URI типа ошибки;
  • title: короткое человекочитаемое название;
  • status: HTTP status code;
  • detail: конкретное описание ошибки;
  • instance: path и query текущего request-а;
  • code: стабильный backend-код ошибки для frontend-логики;
  • request_id: id запроса из X-Request-Id.

Frontend не должен парсить title или detail для бизнес-логики. Для логики используется только code и дополнительные машинные поля.

Ошибка валидации query params

Для некорректного association_type:

HTTP status:

400 Bad Request

Response:

{
  "type": "https://api.example.ru/problems/invalid-association-type",
  "title": "Invalid association_type",
  "status": 400,
  "detail": "Unsupported association_type: all",
  "instance": "/api/v1/competence-map/association-map/?association_type=all",
  "code": "invalid_association_type",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
  "received_value": "all",
  "allowed_values": ["clusters", "technoparks", "sez"]
}

Для несуществующего фильтра:

HTTP status:

400 Bad Request

Response:

{
  "type": "https://api.example.ru/problems/invalid-filter-value",
  "title": "Invalid filter value",
  "status": 400,
  "detail": "Unknown or unavailable filter value for region_id",
  "instance": "/api/v1/competence-map/association-map/?association_type=clusters&region_id=unknown",
  "code": "invalid_filter_value",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
  "field": "region_id",
  "received_value": "unknown"
}

Для конфликтующих фильтров:

HTTP status:

400 Bad Request

Response:

{
  "type": "https://api.example.ru/problems/conflicting-filters",
  "title": "Conflicting filters",
  "status": 400,
  "detail": "region_id cannot be used together with region_ids",
  "instance": "/api/v1/competence-map/association-map/?association_type=clusters&region_id=RU-MOS&region_ids=RU-MOW",
  "code": "conflicting_filters",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
  "fields": ["region_id", "region_ids"]
}

Для неизвестного query param:

HTTP status:

400 Bad Request

Response:

{
  "type": "https://api.example.ru/problems/unknown-query-param",
  "title": "Unknown query parameter",
  "status": 400,
  "detail": "Unknown query parameter: district",
  "instance": "/api/v1/competence-map/association-map/?district=central",
  "code": "unknown_query_param",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
  "field": "district",
  "allowed_params": [
    "association_type",
    "map_view_mode",
    "federal_district_id",
    "region_id",
    "federal_district_ids",
    "region_ids",
    "industry_id",
    "integrated_structure_id",
    "organization_id",
    "competence_id",
    "limit",
    "offset",
    "sort_field",
    "sort_direction"
  ]
}

Ошибки доступа и инфраструктуры

Для 401, 403, 429, 500, 503 используется тот же формат application/problem+json.

Пример 403 Forbidden:

{
  "type": "https://api.example.ru/problems/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "User does not have access to competence map association map",
  "instance": "/api/v1/competence-map/association-map/",
  "code": "forbidden",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R"
}

Для 429 Too Many Requests backend должен вернуть header:

Retry-After: <seconds>

Ожидаемая схема запросов

При загрузке /new-map:

GET /api/v1/competence-map/association-map/filters/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=clusters

При загрузке /new-map/technoparks:

GET /api/v1/competence-map/association-map/filters/?association_type=technoparks
GET /api/v1/competence-map/association-map/?association_type=technoparks

При загрузке /new-map/sez:

GET /api/v1/competence-map/association-map/filters/?association_type=sez
GET /api/v1/competence-map/association-map/?association_type=sez

При выборе нескольких субъектов РФ на карте:

GET /api/v1/competence-map/association-map/filters/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW
GET /api/v1/competence-map/association-map/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW

При выборе нескольких федеральных округов на карте:

GET /api/v1/competence-map/association-map/filters/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo
GET /api/v1/competence-map/association-map/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo

При переходе к перечню организаций:

GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW&limit=500&offset=0

Не требуется реализовывать отдельные endpoint-ы вида:

GET /api/v1/competence-map/association-map/clusters/
GET /api/v1/competence-map/association-map/technoparks/
GET /api/v1/competence-map/association-map/sez/
GET /api/v1/competence-map/association-map/select-region/
GET /api/v1/competence-map/association-map/select-federal-district/

Если в будущем появятся отдельные действия, например экспорт таблицы организаций или drilldown по строке, для них можно добавить отдельные endpoint-ы. Для текущих состояний страниц из референсных экранов достаточно /association-map/filters/, /association-map/ и /association-map/organizations/.