--- template_id: source-backend-api template_version: 1 source_id: 'sme-support-recipients-registry' source_name: 'Реестр субъектов МСП — получателей поддержки' document_status: review --- # Backend API источника «Реестр субъектов МСП — получателей поддержки» > Уточнение 14.09.2026: разделы подготовки выгрузки с `preparing` / HTTP 202 > заменены [общим runtime-контрактом](../../../source-records-public-runtime-contract.md): > готовый 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: ```text 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: ```text 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 дополнительно содержит: ```text 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//.../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` обязателен для списков. Стандартная ошибка: ```json { "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: ```http GET /api/v1/sources/ Authorization: Bearer ``` Response `200 application/json` содержит среди `data`: ```json { "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: ```http GET /api/v1/sources/sme-support-recipients-registry/ Authorization: Bearer ``` Response `200 application/json`: ```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: ```http GET /api/v1/parsers/dashboard/ Authorization: Bearer ``` Response `200` содержит: ```json { "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: ```http 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 ``` Общие параметры: `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: ```text 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`: ```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: ```http GET /api/v2/organization-source-records/2e5db996-e17d-4f5e-a898-bb29566927e4/ Authorization: Bearer ``` Response `200` повторяет все top-level/list поля и заменяет payload на full detail: ```json { "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: ```http POST /api/v1/sources/sme-support-recipients-registry/refresh/ Authorization: Bearer Content-Type: application/json {} ``` Response `202`: ```json { "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: ```http POST /api/v1/parsers/run/fns_sme_support_recipients/ Authorization: Bearer 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: ```http GET /api/v1/jobs/7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc/ Authorization: Bearer ``` Response `200`: ```json { "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 его не вызывает. ```http POST /api/v1/parsers/upload/fns_sme_support_recipients/ ``` Response `405`: ```json { "success": false, "errors": [ { "code": "upload_not_supported", "message": "Источник обновляется из официального Open Data URL.", "field": null } ] } ``` ## 11. История обновлений ### 11.1 `GET /api/v1/system/logs/` Request: ```http GET /api/v1/system/logs/?source=fns_sme_support_recipients&page=1&page_size=20&ordering=-created_at Authorization: Bearer ``` Response `200`: ```json { "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: ```http GET /api/v1/system/logs/5102/ Authorization: Bearer ``` Response `200` добавляет: ```json { "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: ```http GET /api/v1/system/logs/export/?source=fns_sme_support_recipients&format=csv Authorization: Bearer ``` 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: ```json { "sources": ["government_support"], "format": "xlsx" } ``` Response `202`: ```json { "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: ```json { "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-варианта: ```json { "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: ```text 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 выполняет: ```bash 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 | Открыто |