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

1083 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/<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` обязателен для
списков.
Стандартная ошибка:
```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 <token>
```
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 <token>
```
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 <token>
```
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 <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:
```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 <token>
```
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 <token>
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 <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:
```http
GET /api/v1/jobs/7b4d7911-5f1a-4fad-b2bb-67e9c170e4cc/
Authorization: Bearer <token>
```
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 <token>
```
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 <token>
```
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 <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:
```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 | Открыто |