Files
mostovik-backend/docs/source-integration/sources/sme-support-recipients-registry/backend-api.md
Aleksandr Meshchryakov 18971d33ec
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s
feat: complete published registry contracts and gated SRO ingestion
2026-09-14 17:01:02 +02:00

59 KiB
Raw Blame History

template_id, template_version, source_id, source_name, document_status
template_id template_version source_id source_name document_status
source-backend-api 1 sme-support-recipients-registry Реестр субъектов МСП — получателей поддержки review

Backend API источника «Реестр субъектов МСП — получателей поддержки»

Уточнение 14.09.2026: разделы подготовки выгрузки с preparing / HTTP 202 заменены общим runtime-контрактом: готовый ticket HTTP 201 → native form download HTTP 200; устаревшее поколение — HTTP 503 source_export_not_ready, истёкший/использованный ticket — HTTP 410 ticket_expired.

Назначение: backend-first спецификация загрузки официального набора ФНС, нормализации мер поддержки, обогащения организаций и предоставления frontend полного typed list/detail API. Frontend не обращается к rmsp-pp.nalog.ru, не скачивает ZIP/XML и не обходит внешний поиск.

Принципы документа

  • production-источник истины — ежемесячный XML ZIP из Open Data ФНС, а не внутренний JSON API;
  • одна СвПредПод становится одной записью sme_support_measure;
  • list payload лёгкий, detail payload полный и содержит вложенные массивы;
  • Наименование, ИНН, ОГРН и ОКПО обязательны у каждой опубликованной организации;
  • ОКПО и остальные отсутствующие реквизиты дообогащает backend;
  • raw, staging, quarantine и published snapshot имеют раздельные счётчики;
  • предыдущий published snapshot сохраняется до атомарного завершения нового импорта;
  • универсальные endpoint расширяются, source-specific URL не создаются;
  • идентификаторы и decimal передаются строками, даты — ISO 8601/date;
  • openapi.json, runtime-response и contract tests должны совпадать до передачи frontend;
  • display-строки, HTML и пять nullable-полей размера вместо массива backend не возвращает.

1. Назначение и область реализации

Описание источника

Параметр Значение
Наименование Реестр субъектов МСП — получателей поддержки
Назначение Сведения о получателе, решении, форме/виде/размере поддержки, поставщике, нормативных документах и нарушениях
Владелец backend Команда backend/data Mostovik
Внешняя система ФНС России: nalog.gov.ru/opendata/7707329152-rsmppp и rmsp-pp.nalog.ru
Способ получения Discovery metadata → скачивание ZIP → потоковый XML parser → XSD/semantic validation
Периодичность Ежемесячный snapshot; ежедневная проверка нового файла после 15-го числа
Наблюдаемый объём 15.08.2026 12 580 017 мер, 3 287 971 получатель, ZIP около 802 MiB
Ограничения web-поиска До 30 000 ИНН/результатов, page size до 100, XLSX до 10 000; не production transport
Retention Raw archive/checksum/provenance и минимум два последних snapshot; published переключается атомарно

Официальные upstream URL:

https://www.nalog.gov.ru/opendata/7707329152-rsmppp/
https://file.nalog.ru/opendata/7707329152-rsmppp/structure-20230615.xsd
https://file.nalog.ru/opendata/7707329152-rsmppp/VO_SVMSP_2_213_23_04_04.docx
https://rmsp-pp.nalog.ru/search.html?m=SupportList
https://rmsp-pp.nalog.ru/statistics.html

Наблюдаемый upstream-контракт

XSD 4.04 задаёт Файл → Документ → СвЮЛ/СвФЛ → СвПредПод[1..N].

Получатель-ЮЛ содержит обязательные НаимОрг, ИНН и ОГРН. Мера поддержки содержит:

  • строковый реестровый номер до 36 символов;
  • дату внесения сведений, тип получателя и категорию МСП на дату решения;
  • название и ИНН поставщика;
  • дату решения, срок поддержки и необязательную дату прекращения;
  • форму и вид поддержки как код + название;
  • РазмПод[1..N]: decimal(18,2) и единица 1..5;
  • признак и массив нарушений;
  • массив нормативных документов;
  • необязательную дату последнего изменения.

В проверенной web-выборке 200 ИНН найдено 46 получателей и 347 мер; на одного получателя приходилось от 1 до 33 мер. Все 347 строк имели разные dt_create и dt_insert. Пример 45316518: JSON dt_create=15.10.2024, dt_insert=04.10.2024 14:59:07, а XLSX показывает дату внесения 04.10.2024. Поэтому web-поля служат QA fixture, а production date mapping строится только от XML.

Покрываемые frontend-сценарии

Сценарий Требуется Страница / элемент Endpoint
Каталог Да /sources, карточка /api/v1/sources/*
Главная аналитика Да /main, source/coverage /api/v1/parsers/dashboard/
Detail источника Да /sources/sme-support-recipients-registry source detail + records
Таблица/карточка организации Да source detail и /organizations/:uid v2 source records
Полный detail записи Да диалог записи v2 record detail
Ручное обновление Да /settings/scraping refresh/parser run/jobs
История Да /update-history system logs
Выгрузка Да настройки источников export ticket/download
File upload Нет upstream публикует URL upload endpoint возвращает documented 405

2. Нормативные ссылки и аудит текущих контрактов

Уровень Репозиторный путь Что изменить/проверить
Backend-first эталон docs/info/backend-endpoints-main-page-from-mocks.md envelope, statuses, jobs и counts
Машинная схема openapi.json новые enum, filters, ordering, named list/detail payload
Orval orval.config.ts стабильные operationId и generation
Generated TypeScript src/shared/api/generated-api/ DTO/functions list/detail/dashboard/refresh/export
Generated Zod src/shared/model/generated-zod/ required/nullable/enums/nested arrays
Runtime adapters src/pages/main/model/ удалить временные DTO после генерации
Contract tests src/pages/main/model/__contract__/ фактические responses стенда
Шаблоны источников docs/source-integration/templates/ структура документов версии 1/1/2

Точки сверки по каждому активному endpoint

Endpoint Текущее состояние Целевое состояние
GET /api/v1/sources/ Универсальный каталог Новая карточка и точные counts
GET /api/v1/sources/{slug}/ Универсальный detail Source item, snapshot/raw/quarantine metrics
GET /api/v1/parsers/dashboard/ OpenAPI schema неполная Полностью типизированный parser/dashboard snapshot
GET /api/v2/organization-source-records/ Generic payload и ограниченные enum Light typed payload, filters/orderings для поддержки
GET /api/v2/organization-source-records/{uid}/ Generic detail schema Full SmeSupportRecordDetailPayload
Refresh/jobs/logs/export Универсальные процессы Добавить идентификаторы источника и metrics

Реестр расхождений этого источника

Расхождение Решение
XLSX содержит 27 плоских колонок, XSD допускает массивы В storage/API размеры, документы и нарушения остаются массивами
dt_create и dt_insert web API расходятся Канонична ДатаСвед XML; web timestamps только diagnostic fixture
XML не содержит ОКПО, КПП, адрес, ОКВЭД и регион Enrichment из канонического реестра, provenance canonical_organization
XML включает ИП/КФХ/НПД, текущий API требует ОГРН+ОКПО Сохранить raw/staging; первая итерация публикует только ЮЛ
id == support_regnum в выборке Не полагаться на числовой id; внешний ключ — строковый реестровый номер
row_cnt повторяет total query Не хранить в записи
Код единицы 5 наблюдался со значением больше 1 Не терять raw; semantic warning/quarantine по решению после production fixture
amount generic API не выражает часы/проценты/единицы amount только RUB; UI использует support_sizes[]

3. Идентификаторы источника

Идентификатор Значение Использование
source_id sme-support-recipients-registry Папка и спецификация
parentSlug sme-support-recipients-registry /sources/*
routeSlug sme-support-recipients-registry Frontend route
parserSource fns_sme_support_recipients dashboard, logs, source record source
sourceGroup government_support records и export enum
recordType sme_support_measure Классификация записи
sourceItemCode fns_sme_support_recipients source_items[].code
refreshKey fns_sme_support_recipients source refresh/parser run
taskName parsers.fns_sme_support_recipients.refresh Jobs metadata
tableKey sme-support-records Только frontend

Допустимые aliases на период миграции: sme-support-registry, fns-sme-support-recipients. Backend всегда отвечает каноническими значениями. uid source record, XML ИдДок, номер поддержки и organization.uid — разные идентификаторы.

Стабильный ключ записи: (parserSource, support_registry_number). При обнаружении одного номера у разных получателей snapshot блокируется как integrity conflict; UUID записи при повторной загрузке того же номера не меняется.

4. Обязательный контракт организации

Пользовательское поле API-поле Тип Required Nullable Правило
Наименование organization.name string Да Нет Каноническое непустое название
ИНН organization.inn string Да Нет Ровно 10 цифр для опубликованного ЮЛ
ОГРН organization.ogrn string Да Нет Ровно 13 цифр для опубликованного ЮЛ
ОКПО organization.okpo string Да Нет 8/10 цифр по каноническому реестру

Дополнительно detail может вернуть nullable kpp, legal_address, business_activity, region, но эти поля не заменяют обязательную четвёрку.

Происхождение и нормализация обязательных полей

Поле Upstream Алгоритм При отсутствии/конфликте
organization.name СвЮЛ.НаимОрг trim/Unicode/quote normalization, затем каноническое имя organization_not_found либо identity_conflict
organization.inn СвЮЛ.ИННЮЛ строка, контрольный разряд, без numeric cast invalid_inn
organization.ogrn СвЮЛ.ОГРН строка, контрольный разряд, основной exact lookup invalid_ogrn
organization.okpo отсутствует exact ОГРН → проверка ИНН → канонический ОКПО okpo_missing

Порядок enrichment: exact ОГРН, подтверждение ИНН, fallback exact ИНН только при единственном кандидате, проверка имени как сигнал, но не как самостоятельный ключ. Любое неоднозначное совпадение уходит в quarantine. Поставщик поддержки обогащается отдельно и не блокирует публикацию основной записи.

Source-specific модель

List payload:

support_registry_number, recipient_type, sme_category, region,
support_form, support_kind, decision_date, support_until, termination_date,
registry_entry_date, source_updated_date, support_sizes[], provider,
has_violation, violation_count, regulatory_documents_count, source_snapshot_date

Detail дополнительно содержит:

source_document_id, organization_profile, provider enrichment,
regulatory_documents[], violations[], provenance

Dictionary value: { code: string, name: string }. Decimal size: строка с максимум двумя знаками после точки. support_sizes не бывает пустым. Единицы нормализуются:

Код unit Название
1 RUB рубль
2 square_meter квадратный метр
3 hour час
4 percent процент
5 unit единица

5. Матрица endpoint и решение по реализации

Метод и endpoint Статус Изменение Потребитель
1 GET /api/v1/sources/ Нужно расширить Карточка, counts, snapshot /sources, /main
2 GET /api/v1/sources/{slug}/ Нужно расширить Source item и source-specific metrics Detail/scraping
3 GET /api/v1/parsers/dashboard/ Нужно расширить Typed source/dashboard/coverage Main/detail/scraping
4 GET /api/v2/organization-source-records/ Нужно расширить Enum, filters, ordering, light payload Таблицы
5 GET /api/v2/organization-source-records/{uid}/ Нужно расширить Full nested detail Диалог записи
6 POST /api/v1/sources/{slug}/refresh/ Нужно расширить Одиночный refresh без params Scraping
7 POST /api/v1/parsers/run/{source_key}/ Нужно расширить Direct parser run Internal/settings
8 GET /api/v1/jobs/{task_id}/ Существует и подходит Source-specific progress meta Polling
9 GET /api/v1/system/logs/ Нужно расширить Фильтр/metrics parserSource History
10 GET /api/v1/system/logs/{id}/ Нужно расширить Snapshot/quarantine detail History detail
11 GET /api/v1/system/logs/export/ Нужно расширить История нового parserSource CSV
12 POST /api/v2/organization-source-records/export-ticket/ Нужно расширить Enum government_support Export
13 POST /api/v2/organization-source-records/export-download/ Существует и подходит Без source-specific логики Export

Существующие, но не обязательные endpoint:

  • GET /api/v1/sources/statuses/ — не использовать: карточки и dashboard уже содержат статус;
  • POST /api/v2/organization-source-records/export/ — синхронный export не использовать в UI, но контракт сохранить документированным;
  • GET /api/v1/parsers/sources/ — не использовать, metadata приходит dashboard;
  • GET/POST /api/v1/parsers/schedules/* — не вызывать frontend, schedule приходит dashboard;
  • GET /api/v1/parsers/load-logs/ — не использовать, история через system logs;
  • GET /api/v1/parsers/records/ — не использовать, таблица через v2 records;
  • GET /api/v1/parsers/results/{source_key}/* — legacy, не добавлять;
  • POST /api/v1/parsers/upload/{source_key}/ — неприменим, источник URL-based;
  • GET /api/v1/jobs/ — неприменим, polling по detail job;
  • POST /api/v1/jobs/{task_id}/control/ и GET /api/v1/jobs/{task_id}/stream/ — не требуются;
  • legacy download GET /api/v2/sources/<legacy-source>/.../download/ — не добавлять.

6. Общий паттерн описания endpoint

Все endpoint требуют bearer-auth. JSON responses используют проектный envelope, кроме бинарного download. Date — YYYY-MM-DD, datetime — ISO 8601 с timezone. Пустые массивы — [], не null. Unknown counter — null, но известный ноль — 0. meta.pagination обязателен для списков.

Стандартная ошибка:

{
  "success": false,
  "errors": [
    {
      "code": "validation_error",
      "message": "Некорректный параметр source.",
      "field": "source"
    }
  ]
}

Общие коды: 400 validation, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict/already running, 422 unsupported filter combination, 429 throttled, 500 server. GET можно повторять; POST refresh идемпотентен на время активной задачи.

7. Каталог источников

7.1 GET /api/v1/sources/

Request:

GET /api/v1/sources/
Authorization: Bearer <token>

Response 200 application/json содержит среди data:

{
  "slug": "sme-support-recipients-registry",
  "title": "Реестр субъектов МСП — получателей поддержки",
  "description": "Меры государственной поддержки юридических лиц из официального реестра ФНС.",
  "order": 70,
  "is_available": true,
  "status": "success",
  "status_label": "Обновлено",
  "progress": 100,
  "records_count": 8421050,
  "organizations_count": 812340,
  "last_updated_at": "2026-08-16T03:42:10+03:00",
  "next_update_at": "2026-09-15T02:00:00+03:00",
  "error_message": "",
  "task_names": ["parsers.fns_sme_support_recipients.refresh"],
  "refresh_requires_params": false,
  "refresh_params": []
}

records_count — опубликованные меры ЮЛ; organizations_count — distinct canonical organization.uid. Значения примера иллюстративны. last_updated_at — время публикации нашего snapshot, не upstream ДатаСост. Сортировка каталога: order, затем slug.

Ошибки: 401 detail auth, 429 standard envelope, 500 standard envelope; повтор GET допустим.

7.2 GET /api/v1/sources/{slug}/

Request:

GET /api/v1/sources/sme-support-recipients-registry/
Authorization: Bearer <token>

Response 200 application/json:

{
  "success": true,
  "errors": null,
  "meta": {},
  "data": {
    "slug": "sme-support-recipients-registry",
    "title": "Реестр субъектов МСП — получателей поддержки",
    "description": "Меры государственной поддержки юридических лиц из официального реестра ФНС.",
    "status": "success",
    "status_label": "Обновлено",
    "progress": 100,
    "records_count": 8421050,
    "organizations_count": 812340,
    "last_updated_at": "2026-08-16T03:42:10+03:00",
    "next_update_at": "2026-09-15T02:00:00+03:00",
    "error_message": "",
    "task_names": ["parsers.fns_sme_support_recipients.refresh"],
    "refresh_requires_params": false,
    "refresh_params": [],
    "active_tasks": [],
    "source_metrics": {
      "source_snapshot_date": "2026-08-15",
      "raw_support_records_count": 12580017,
      "raw_recipients_count": 3287971,
      "raw_legal_entities_count": 862467,
      "published_records_count": 8421050,
      "published_organizations_count": 812340,
      "quarantined_records_count": 18420,
      "unsupported_recipient_records_count": 4140547,
      "violations_count": 11759
    },
    "source_items": [
      {
        "code": "fns_sme_support_recipients",
        "refresh_key": "fns_sme_support_recipients",
        "title": "Реестр субъектов МСП — получателей поддержки",
        "parser_source": "fns_sme_support_recipients",
        "parser_source_display": "Реестр субъектов МСП — получателей поддержки",
        "records_count": 8421050,
        "organizations_count": 812340,
        "last_updated_at": "2026-08-16T03:42:10+03:00",
        "latest_load": {
          "batch_id": 5102,
          "source": "fns_sme_support_recipients",
          "records_count": 8421050,
          "status": "success",
          "error_message": "",
          "created_at": "2026-08-16T01:10:00+03:00",
          "updated_at": "2026-08-16T03:42:10+03:00"
        },
        "latest_success_load": {
          "batch_id": 5102,
          "source": "fns_sme_support_recipients",
          "records_count": 8421050,
          "status": "success",
          "error_message": "",
          "created_at": "2026-08-16T01:10:00+03:00",
          "updated_at": "2026-08-16T03:42:10+03:00"
        }
      }
    ],
    "latest_load": null,
    "latest_success_load": null
  }
}

source_metrics values are integer >=0. unsupported_recipient_records_count includes measures ИП/КФХ/НПД excluded from organization API. 404 returns source_not_found. active_tasks[] uses job fields task_id, task_name, status, progress, progress_message, started_at, created_at, meta.

8. Dashboard парсеров

8.1 GET /api/v1/parsers/dashboard/

Request:

GET /api/v1/parsers/dashboard/
Authorization: Bearer <token>

Response 200 содержит:

{
  "success": true,
  "data": {
    "sources": [
      {
        "key": "fns_sme_support_recipients",
        "source": "fns_sme_support_recipients",
        "title": "Реестр субъектов МСП — получателей поддержки",
        "agency": "ФНС России",
        "task_name": "parsers.fns_sme_support_recipients.refresh",
        "is_existing": true,
        "requires_file_url": false,
        "mode": "scheduled",
        "status": "active",
        "access_method": "open_data_xml_zip",
        "parser_strategy": "atomic_snapshot",
        "supports_file_upload": false,
        "result_list_url": "/api/v2/organization-source-records/?source=fns_sme_support_recipients",
        "result_detail_url": "/api/v2/organization-source-records/{uid}/"
      }
    ],
    "source_counts": { "fns_sme_support_recipients": 8421050 },
    "load_logs": [],
    "schedules": [
      {
        "key": "fns_sme_support_recipients",
        "name": "parsers.fns_sme_support_recipients.refresh",
        "enabled": true,
        "schedule_type": "crontab",
        "schedule": { "minute": "0", "hour": "2", "day_of_month": "15-31" }
      }
    ],
    "registry_enrichment_analytics": {
      "population": { "active_registry_organizations": 1500000 },
      "source_coverage": [
        {
          "source": "fns_sme_support_recipients",
          "label": "Реестр субъектов МСП — получателей поддержки",
          "records_count": 8421050,
          "organizations_count": 125000,
          "coverage_percent": 8.3333,
          "last_updated_at": "2026-08-16T03:42:10+03:00"
        }
      ],
      "risk_signals": []
    }
  }
}

source_coverage[].organizations_count — пересечение опубликованных получателей с организациями ОПК, а /sources[].organizations_count — все уникальные опубликованные организации источника. Coverage не превышает population.active_registry_organizations; процент вычисляется от числа организаций ОПК. source_counts совпадает с published records. Полная OpenAPI schema обязательна, data: void не допускается. Ошибки: 401, 429, 500.

9. Записи источника

9.1 GET /api/v2/organization-source-records/

Request:

GET /api/v2/organization-source-records/?source_group=government_support&source=fns_sme_support_recipients&record_type=sme_support_measure&page=1&page_size=50&ordering=-record_date
Authorization: Bearer <token>

Общие параметры: organization UUID, search max 200, date_from/date_to по decision date, page>=1, page_size default 50/max 100.

Source-specific filters:

Параметр Тип Правило
recipient_type enum legal_entity, individual_entrepreneur, farm, self_employed; published MVP фактически ЮЛ
sme_category enum micro, small, medium, not_sme
region_code string Двузначный код обогащённой организации
support_form_code string Точный upstream-код
support_kind_code string Точный upstream-код
provider_inn string 10 цифр
has_violation boolean true/false
support_unit enum RUB, square_meter, hour, percent, unit
amount_from/amount_to decimal string Только RUB; включительно
registry_entry_from/to date Дата внесения XML
support_until_from/to date Срок поддержки
termination_from/to date Дата прекращения

search ищет по name/ИНН/ОГРН/ОКПО, номеру поддержки, provider name/ИНН, form/kind. Ordering whitelist:

record_date, external_id, extension__organization__name,
extension__organization__inn, extension__organization__ogrn,
extension__organization__okpo, payload__support_form__name,
payload__support_kind__name, payload__support_until,
payload__provider__name, payload__sme_category__name,
payload__has_violation, amount

Каждое поле допускает -; default -record_date,-external_id, nulls last.

Response 200 application/json:

{
  "success": true,
  "errors": null,
  "data": [
    {
      "uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
      "extension_uid": "3e4c092e-81fb-4d41-9acb-f77922724339",
      "source_group": "government_support",
      "record_type": "sme_support_measure",
      "source": "fns_sme_support_recipients",
      "external_id": "45316518",
      "title": "Иные консультационные услуги",
      "record_date": "2024-09-27",
      "amount": null,
      "status": "published",
      "url": "https://rmsp-pp.nalog.ru/subject.html?id=5030056754&id2=1075030001023#support=45316518",
      "payload": {
        "support_registry_number": "45316518",
        "recipient_type": { "code": "1", "name": "Юридическое лицо" },
        "sme_category": { "code": "1", "name": "Микропредприятие" },
        "region": { "code": "77", "name": "Москва", "provenance": "canonical_organization" },
        "support_form": { "code": "0400", "name": "Консультационная поддержка" },
        "support_kind": { "code": "0401", "name": "Иные консультационные услуги" },
        "decision_date": "2024-09-27",
        "support_until": "2024-09-27",
        "termination_date": null,
        "registry_entry_date": "2024-10-04",
        "source_updated_date": null,
        "support_sizes": [{ "unit_code": "3", "unit": "hour", "value": "1.00" }],
        "provider": {
          "name": "АНО \"МОСКОВСКИЙ ЭКСПОРТНЫЙ ЦЕНТР\"",
          "inn": "7710012211"
        },
        "has_violation": false,
        "violation_count": 0,
        "regulatory_documents_count": 1,
        "source_snapshot_date": "2026-08-15"
      },
      "legacy_model": "",
      "legacy_pk": "",
      "load_batch": 5102,
      "created_at": "2026-08-16T03:39:00+03:00",
      "updated_at": "2026-08-16T03:39:00+03:00",
      "financial_lines": [],
      "organization": {
        "uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
        "name": "АО \"90 ЭКСПЕРИМЕНТАЛЬНЫЙ ЗАВОД\"",
        "full_name": "АКЦИОНЕРНОЕ ОБЩЕСТВО \"90 ЭКСПЕРИМЕНТАЛЬНЫЙ ЗАВОД\"",
        "inn": "5030056754",
        "kpp": "773001001",
        "ogrn": "1075030001023",
        "okpo": "07546844",
        "legal_address": "г. Москва",
        "business_activity": "Производство"
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "page_size": 50,
      "total_count": 2,
      "total_pages": 1,
      "has_next": false,
      "has_previous": false
    }
  }
}

Примерные enriched КПП/ОКПО/адрес выше не являются утверждением о фактических реквизитах организации; contract fixture backend обязан заменить их значениями тестового канонического реестра. amount=null, потому что мера выражена в часах. Для RUB amount совпадает с суммой RUB-элементов support_sizes.

9.2 GET /api/v2/organization-source-records/{uid}/

Request:

GET /api/v2/organization-source-records/2e5db996-e17d-4f5e-a898-bb29566927e4/
Authorization: Bearer <token>

Response 200 повторяет все top-level/list поля и заменяет payload на full detail:

{
  "success": true,
  "errors": null,
  "meta": {},
  "data": {
    "uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
    "source_group": "government_support",
    "record_type": "sme_support_measure",
    "source": "fns_sme_support_recipients",
    "external_id": "45316518",
    "title": "Иные консультационные услуги",
    "record_date": "2024-09-27",
    "amount": null,
    "status": "published",
    "url": "https://rmsp-pp.nalog.ru/subject.html?id=5030056754&id2=1075030001023#support=45316518",
    "organization": {
      "uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
      "name": "АО \"90 ЭКСПЕРИМЕНТАЛЬНЫЙ ЗАВОД\"",
      "full_name": "АКЦИОНЕРНОЕ ОБЩЕСТВО \"90 ЭКСПЕРИМЕНТАЛЬНЫЙ ЗАВОД\"",
      "inn": "5030056754",
      "kpp": "773001001",
      "ogrn": "1075030001023",
      "okpo": "07546844",
      "legal_address": "г. Москва",
      "business_activity": "Производство"
    },
    "payload": {
      "support_registry_number": "45316518",
      "source_document_id": "VO-TEST-5030056754",
      "recipient_type": { "code": "1", "name": "Юридическое лицо" },
      "sme_category": { "code": "1", "name": "Микропредприятие" },
      "region": { "code": "77", "name": "Москва", "provenance": "canonical_organization" },
      "support_form": { "code": "0400", "name": "Консультационная поддержка" },
      "support_kind": { "code": "0401", "name": "Иные консультационные услуги" },
      "decision_date": "2024-09-27",
      "support_until": "2024-09-27",
      "termination_date": null,
      "registry_entry_date": "2024-10-04",
      "source_updated_date": null,
      "support_sizes": [{ "unit_code": "3", "unit": "hour", "value": "1.00" }],
      "provider": {
        "name": "АНО \"МОСКОВСКИЙ ЭКСПОРТНЫЙ ЦЕНТР\"",
        "inn": "7710012211",
        "organization_uid": null,
        "okpo": null,
        "region": null,
        "oktmo": null
      },
      "regulatory_documents": [
        {
          "kind": null,
          "type": "Закон города Москвы",
          "adopting_authority": "Московская городская Дума",
          "date": "2008-11-26",
          "number": "60",
          "name": "О поддержке и развитии малого и среднего предпринимательства в городе Москве"
        }
      ],
      "has_violation": false,
      "violations": [],
      "provenance": {
        "dataset_url": "https://www.nalog.gov.ru/opendata/7707329152-rsmppp/",
        "archive_name": "data-20260815-structure-20230615.zip",
        "archive_sha256": "00bc1d1ef97e1332f5e59fb04fd230f7ed0e0452ba7e90f6551d357ec891ce7a",
        "source_snapshot_date": "2026-08-15",
        "format_version": "4.04",
        "xsd_name": "structure-20230615.xsd",
        "load_batch": 5102,
        "imported_at": "2026-08-16T03:39:00+03:00"
      }
    }
  }
}

regulatory_documents и violations всегда массивы. Violation object содержит type dictionary, recognized_date, remedy_deadline, remedied_date. Provider enrichment nullable и не подменяет upstream name/ИНН. 404 record_not_found; запись из другого source возвращается по UID корректно, а frontend проверяет source/tableKey.

10. Refresh и фоновые задачи

10.1 POST /api/v1/sources/{slug}/refresh/

Request:

POST /api/v1/sources/sme-support-recipients-registry/refresh/
Authorization: Bearer <token>
Content-Type: application/json

{}

Response 202:

{
  "success": true,
  "errors": null,
  "data": {
    "task_id": "7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc",
    "task_ids": ["7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc"],
    "status": "queued"
  }
}

Только роль с refresh permission. 409 already_running возвращает существующий task_id в meta.active_task_id. Параметры URL/file запрещены.

10.2 POST /api/v1/parsers/run/{source_key}/

Request:

POST /api/v1/parsers/run/fns_sme_support_recipients/
Authorization: Bearer <token>
Content-Type: application/json

{}

Response 202 имеет тот же task_id/task_ids контракт. Неизвестный alias — 404 parser_source_not_found; активный запуск — 409 already_running. Endpoint не принимает путь локального файла или произвольный upstream URL.

10.3 GET /api/v1/jobs/{task_id}/

Request:

GET /api/v1/jobs/7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc/
Authorization: Bearer <token>

Response 200:

{
  "success": true,
  "errors": null,
  "data": {
    "task_id": "7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc",
    "task_name": "parsers.fns_sme_support_recipients.refresh",
    "status": "running",
    "progress": 62,
    "progress_message": "Обогащение организаций",
    "created_at": "2026-08-16T01:10:00+03:00",
    "started_at": "2026-08-16T01:10:04+03:00",
    "finished_at": null,
    "result": null,
    "error": null,
    "meta": {
      "batch_id": 5102,
      "source_snapshot_date": "2026-08-15",
      "raw_records": 12580017,
      "normalized_records": 8421050,
      "quarantined_records": 18420
    }
  }
}

Terminal success result повторяет final counts/checksum; failed содержит structured error. progress монотонен 0..100. 404 job_not_found.

10.4 POST /api/v1/parsers/upload/{source_key}/ (условный)

Request для источника неприменим: frontend его не вызывает.

POST /api/v1/parsers/upload/fns_sme_support_recipients/

Response 405:

{
  "success": false,
  "errors": [
    {
      "code": "upload_not_supported",
      "message": "Источник обновляется из официального Open Data URL.",
      "field": null
    }
  ]
}

11. История обновлений

11.1 GET /api/v1/system/logs/

Request:

GET /api/v1/system/logs/?source=fns_sme_support_recipients&page=1&page_size=20&ordering=-created_at
Authorization: Bearer <token>

Response 200:

{
  "success": true,
  "errors": null,
  "data": [
    {
      "id": 5102,
      "source": "fns_sme_support_recipients",
      "source_display": "Реестр субъектов МСП — получателей поддержки",
      "status": "success",
      "records_count": 8421050,
      "organizations_count": 812340,
      "error_message": "",
      "created_at": "2026-08-16T01:10:00+03:00",
      "updated_at": "2026-08-16T03:42:10+03:00"
    }
  ],
  "meta": { "pagination": { "page": 1, "page_size": 20, "total_count": 1, "total_pages": 1 } }
}

Filters: source, status, date_from/to; ordering created_at/updated_at/id. Response counts — published, source-specific raw/quarantine находятся в detail.

11.2 GET /api/v1/system/logs/{id}/

Request:

GET /api/v1/system/logs/5102/
Authorization: Bearer <token>

Response 200 добавляет:

{
  "success": true,
  "errors": null,
  "data": {
    "id": 5102,
    "source": "fns_sme_support_recipients",
    "status": "success",
    "records_count": 8421050,
    "organizations_count": 812340,
    "source_snapshot_date": "2026-08-15",
    "archive_name": "data-20260815-structure-20230615.zip",
    "archive_sha256": "00bc1d1ef97e1332f5e59fb04fd230f7ed0e0452ba7e90f6551d357ec891ce7a",
    "raw_records_count": 12580017,
    "quarantined_records_count": 18420,
    "quarantine_by_reason": { "okpo_missing": 17400, "identity_conflict": 1020 },
    "warnings": [],
    "error_message": ""
  }
}

404 log_not_found. Не возвращать raw XML с персональными данными.

11.3 GET /api/v1/system/logs/export/

Request:

GET /api/v1/system/logs/export/?source=fns_sme_support_recipients&format=csv
Authorization: Bearer <token>

Response 200 text/csv; charset=utf-8 с Content-Disposition. Колонки включают batch, source, status, snapshot, published/raw/quarantine counts, timestamps и error. Ошибки 400, 401, 403, 500 возвращаются JSON до начала streaming.

12. Выгрузка данных источника

12.1 POST /api/v2/organization-source-records/export-ticket/

Request:

{
  "sources": ["government_support"],
  "format": "xlsx"
}

Response 202:

{
  "success": true,
  "errors": null,
  "data": { "ticket": "exp_f743e84b", "status": "preparing", "expires_at": "2026-08-26T14:00:00Z" }
}

Те же filters records могут быть переданы документированным filters object. Ticket привязан к пользователю и истекает.

12.2 POST /api/v2/organization-source-records/export-download/

Request:

{ "ticket": "exp_f743e84b" }

Response 200 application/zip или выбранный MIME с Content-Disposition. 202 означает ещё не готово, 404 неизвестный ticket, 410 истёкший, 403 чужой ticket.

12.3 POST /api/v2/organization-source-records/export/

Request синхронного legacy-варианта:

{ "sources": ["government_support"], "format": "json" }

Response 200 допустим только для малого результата; frontend не вызывает endpoint. Для большого набора response 413 export_too_large с рекомендацией ticket flow.

CSV/XLSX содержит обязательные organization fields и 27 бизнес-колонок, совместимых по смыслу с выгрузкой ФНС. Если у меры несколько размеров/документов/нарушений, JSON сохраняет массивы, а XLSX/CSV создаёт отдельные листы/таблицы с foreign key support_registry_number; данные не склеиваются в неразбираемую строку.

13. Статусы и жизненный цикл

Operational job statuses:

queued → pending → running → success
                    ↘ retrying → running
                    ↘ failed
                    ↘ cancelled

UI: активные → Выполняется, терминальные → Обновлено; failure message показывается отдельно. Business record status:

  • published — мера есть в активном snapshot;
  • terminated — XML содержит дату прекращения;
  • запись, исчезнувшая из нового полного snapshot, архивируется и не входит в active list.

Не вычислять active/completed только по support_until: upstream не гарантирует такую семантику. Violation — отдельный признак, не status.

14. Согласованность данных между endpoint

  • /sources.records_count = source_item.records_count = dashboard.source_counts = published list meta.pagination.total_count для одинакового snapshot/filters;
  • /sources.organizations_count — distinct всех canonical organizations источника;
  • source_coverage[].organizations_count — только организации ОПК и не обязано совпадать с /sources[].organizations_count;
  • external_id = payload.support_registry_number;
  • record_date = payload.decision_date;
  • amount равен RUB-size либо null и никогда не содержит часы/проценты;
  • list/detail organization identity и core payload совпадают;
  • violation_count = detail.violations.length;
  • regulatory_documents_count = detail.regulatory_documents.length;
  • last_updated_at всех metadata относится к одной публикации snapshot;
  • raw, unsupported, quarantined и published counts не суммируются без документированной формулы; official statistics используются только для reconciliation.

15. Ошибки, доступ, производительность и ограничения

Операция Read role Refresh role Admin
Catalog/dashboard/records/detail Да Да Да
Export По отдельному permission Да Да
Refresh/parser run Нет Да Да
Job/log detail Да для разрешённых источников Да Да
Quarantine detail/raw archive Нет Нет Да

Source-specific ошибки: archive_not_found, checksum_mismatch, zip_invalid, xml_parse_error, xsd_validation_error, snapshot_older_than_active, duplicate_support_id, invalid_inn, invalid_ogrn, organization_not_found, identity_conflict, okpo_missing, invalid_support_size, reconciliation_failed.

Нефункциональные требования

  • ZIP/XML читается потоково, без полного файла/DOM в памяти;
  • download поддерживает timeout/retry/range, checksum проверяется до parse;
  • staging writes bulk, индексы строятся/переключаются вне пользовательского list lock;
  • атомарная публикация snapshot; partial snapshot никогда не виден frontend;
  • индексы: source+record_type+record_date, external ID unique, organization UID, form/kind, provider INN, violation, GIN/JSON indexes только при обосновании;
  • list p95 <= 1.5 s на page 50 при типовом фильтре, detail p95 <= 500 ms после warm cache;
  • search max 200 символов, page size max 100, rate limit документирован;
  • raw archive и персональные данные ИП/НПД не выдаются в organization API;
  • metrics/logs не содержат access token, cookie, XML FIO и локальные filesystem paths;
  • parser повторно обрабатывает тот же checksum идемпотентно и не создаёт дубликаты;
  • official stats/date/file metadata записываются для reconciliation, но не подменяют atomic counts Mostovik.

16. Требования к OpenAPI и generated-клиенту

Добавить/уточнить:

  • enum government_support во всех source group/export schemas;
  • enum/source fns_sme_support_recipients и record type sme_support_measure;
  • все source-specific query filters/orderings списка;
  • отдельные SmeSupportRecordListPayload и SmeSupportRecordDetailPayload;
  • schemas SmeSupportDictionaryValue, SmeSupportSize, SmeSupportProvider, SmeSupportRegulatoryDocument, SmeSupportViolation, SmeSupportProvenance;
  • required и nullable для каждого поля; arrays required/non-null;
  • корректный list response и отдельный detail envelope;
  • typed dashboard, refresh/job/log/export errors и binary content types;
  • operationId, пригодные для Orval без дефисов: например v2_organization_source_records_retrieve.

После обновления backend frontend выполняет:

bun run apigen
bun run type-check
bun run test:contract

Diff generated API проверяется: нельзя вручную редактировать src/shared/api/generated-api/ и src/shared/model/generated-zod/.

17. Backend-тесты и acceptance criteria

Минимальная матрица backend/contract tests

  • XML fixture: ЮЛ с одной и несколькими мерами;
  • одна мера с несколькими размерами разных единиц;
  • zero/one/many нормативных документов и нарушений;
  • termination, update и nullable dates;
  • ИП/КФХ/НПД остаются staging и не публикуются;
  • exact ОГРН+ИНН enrichment, missing ОКПО, ambiguous/conflict;
  • duplicate support number между получателями блокирует snapshot;
  • dt_create/dt_insert/XLSX QA fixture не влияет на XML mapping;
  • код единицы 5 со спорным значением сохраняет raw и создаёт warning/quarantine по политике;
  • повторный import checksum идемпотентен;
  • failed XSD/checksum/reconciliation сохраняет предыдущий snapshot;
  • list filters, ordering, search, organization, date range и pagination;
  • list payload не содержит detail arrays, detail содержит их полностью;
  • counts/invariants между sources/dashboard/list/logs;
  • refresh 202, duplicate 409, job terminal, history и ticket export;
  • permissions, 401/403/404/410/413/422/429/500;
  • performance test на объёме, сопоставимом с 12.6 млн записей.

Backend acceptance criteria

  • Все endpoint и response примеры реализованы и находятся в OpenAPI.
  • Runtime contract tests подтверждают required/nullable/enums/content types.
  • Каждая published запись содержит Наименование, ИНН, ОГРН и ОКПО.
  • Full nested source data не теряется при нормализации и export.
  • Snapshot публикуется атомарно и обратимо.
  • Raw/published/quarantine/unsupported counters согласованы.
  • Frontend может реализовать list/detail/refresh/history/export без прямого upstream-доступа.

18. Чего backend не должен возвращать

  • HTML сайта ФНС, cookies, hash-параметры и токены временной XLSX-выгрузки;
  • row_cnt как поле каждой записи;
  • numeric ИНН/ОГРН/ОКПО/номер поддержки;
  • только одно display-значение вместо support_sizes[];
  • пять nullable size fields в full detail вместо массива;
  • склеенные документы/нарушения вместо typed arrays;
  • "—", "нет данных", форматированные суммы и локализованные даты;
  • raw XML/FIO ИП/НПД в organization endpoint;
  • provider enrichment как замену upstream name/ИНН;
  • live official statistics вместо counts активного snapshot;
  • source-specific legacy endpoints, если универсальный API покрывает сценарий.

19. Решения и открытые вопросы

Вопрос Решение Статус
Production transport Официальный XML ZIP + XSD; web API только QA Принято
Гранулярность Одна мера поддержки = одна запись Принято
ИП/КФХ/НПД Raw/staging, не published в MVP Принято для первой итерации
ОКПО Канонический enrichment; missing/conflict → quarantine Принято
Provider enrichment Nullable, не блокирует публикацию Принято
Дата внесения ДатаСвед XML; fixture подтвердит mapping Требует fixture
Единица 5 > 1 Не терять raw; подтвердить production XSD/данные до strict rejection Открыто
Срок хранения raw Минимум два snapshot; окончательный срок согласовать с data owner Открыто
Official stats reconciliation tolerance Блокировать только на структурной ошибке; числовой threshold согласовать после первого полного import Открыто