70 KiB
template_id, template_version, source_id, source_name, document_status
| template_id | template_version | source_id | source_name | document_status |
|---|---|---|---|---|
| source-backend-api | 1 | budget-process-registry | Реестр участников бюджетного процесса | review |
Backend API источника «Реестр участников бюджетного процесса»
Назначение: backend-first спецификация внутренних API Mostovik, которые дают frontend полный и стабильный клиентский контракт нового источника. Backend самостоятельно взаимодействует с Budget.gov; frontend не знает параметров внешнего API, не обходит его страницы и не парсит raw ответ.
Принципы документа
- использовать существующие универсальные endpoint;
- не создавать source-specific read URL;
- list возвращает только данные таблицы и summary, detail — полный состав записи;
organizationвсегда содержит непустые Наименование, ИНН, ОГРН и ОКПО;- source-specific
payloadописывается named schemas и discriminator, не свободным object; - raw upstream сохраняется backend для аудита, но не является единственным API-контрактом;
- identifiers и classifier codes передаются строками;
- даты передаются ISO 8601/date, boolean — настоящими boolean/null;
- schema, runtime и contract tests должны совпадать до подключения frontend;
- generated API после
bun run apigenдолжен заменить временные frontend DTO; - backend не возвращает UI formatting, labels-заглушки и компоненты.
1. Назначение и область реализации
Описание источника
| Параметр | Значение |
|---|---|
| Наименование | Реестр участников бюджетного процесса |
| Назначение данных | Паспорт, бюджетная принадлежность, полномочия, руководители, деятельность, счета, связи и документы организаций |
| Владелец backend | Команда backend Mostovik |
| Внешняя система / URL | https://budget.gov.ru/epbs/registry/ubpandnubp/data |
| Способ получения | Backend HTTPS GET, пагинация и параметр blocks; frontend-доступ запрещён архитектурно |
| Периодичность | Полный snapshot ежедневно в 03:00 Europe/Moscow и ручной запуск |
| Ожидаемый объём | 352 953 raw records на 24.08.2026; объём динамический |
| Ограничения внешнего API | Фактический максимум pageSize=1000; минимум 353 страницы на проверенном snapshot; публичной OpenAPI нет |
| Политика удаления/актуализации | Исторические versions сохраняются; новый snapshot публикуется атомарно; raw ≥ 90 дней и ≥ 10 snapshots |
Покрываемые frontend-сценарии
| Сценарий | Требуется | Страница / элемент | Endpoint |
|---|---|---|---|
| Каталог источников | Да | /sources |
/api/v1/sources/* |
| Главная аналитика | Да | /main |
/api/v1/parsers/dashboard/ |
| Детальная страница | Да | /sources/budget-process-registry |
source detail/dashboard/records |
| Таблицы записей | Да | Страница источника и организации | /api/v2/organization-source-records/* |
| Ручное обновление | Да | /settings/scraping |
source refresh/parser run/jobs |
| История обновлений | Да | /update-history |
/api/v1/system/logs/* |
| Выгрузка данных | Да | Настройки → выгрузка | ticket/download source export |
2. Нормативные ссылки и аудит текущих контрактов
Нормативная основа: приказ Минфина России № 163н.
Проверенная структура upstream и UI-рекомендации:
docs/info/source-tables-and-budget-registry-analysis.md.
Обязательные точки сверки перед реализацией:
| Уровень | Путь | Требование |
|---|---|---|
| Backend-first эталон | docs/info/backend-endpoints-main-page-from-mocks.md |
Envelope, errors и инварианты |
| Машинная схема | openapi.json |
Paths, required, nullable, enums, MIME |
| Генерация | orval.config.ts |
Стабильные operationId/tags |
| Generated TypeScript | src/shared/api/generated-api/ |
Пригодные DTO/functions без ручного unknown parser |
| Generated Zod | src/shared/model/generated-zod/ |
Runtime validation list/detail |
| Runtime adapter | src/pages/main/model/ |
Удаление временного DTO после генерации |
| Contract tests | src/pages/main/model/__contract__/ |
Проверка стенда против schema |
Точки сверки по каждому активному endpoint
| Endpoint | Текущий контракт | Целевое изменение |
|---|---|---|
GET /api/v1/sources/ |
SourceCardListResponse |
Новая карточка, required counts/date/status |
GET /api/v1/sources/{slug}/ |
SourceCardDetailResponse |
Source item, latest load metadata |
GET /api/v1/parsers/dashboard/ |
200 без полной schema | Именованный DashboardResponse |
GET /api/v2/organization-source-records/ |
Payload только частично типизирован, meta unknown |
Новые enums, list payload и pagination schema |
GET /api/v2/organization-source-records/{uid}/ |
Общий optional DTO | Required common fields и полный detail payload |
POST /api/v1/sources/{slug}/refresh/ |
task_id/task_ids расходятся |
Только task_ids: string[] |
POST /api/v1/parsers/run/{source_key}/ |
201 без schema | Typed task response |
GET /api/v1/jobs/{task_id}/ |
BackgroundJob |
Строгий status enum/result schema |
GET /api/v1/system/logs/* |
list/detail label fields расходятся | source_label в обеих проекциях |
| Export ticket/download | Runtime есть, OpenAPI отсутствует | Описать оба endpoint и ошибки |
Реестр расхождений этого источника
| Endpoint / schema | Runtime сейчас | OpenAPI сейчас | Целевой контракт | Действие backend | Блокирует frontend |
|---|---|---|---|---|---|
| Source registration | Источника нет | Enum/карточки нет | Одна карточка/source item | Зарегистрировать identifiers | Да |
| Records list | Источника нет | Нет budget_process_registry |
Лёгкий typed list | Добавить group/source/type/payload | Да |
| Record detail | Источника нет | Нет detail schema | Полный typed detail | Добавить discriminator/named schemas | Да |
| Pagination | Runtime ожидает meta.pagination |
meta свободный object |
Required pagination | Исправить component schema | Да |
| Required organization | Поля generated optional | Поля optional | Required name/inn/ogrn/okpo | Исправить serializer/schema | Да |
| Dashboard/parser run | Runtime разбирается вручную | Success body не типизирован | Named responses | Обновить OpenAPI/runtime | Да для refresh/analytics |
| Export | Ticket flow вне OpenAPI | Есть неоднозначный sync endpoint | Ticket flow canonical | Описать ticket/download | Да для общего export |
3. Идентификаторы источника
| Идентификатор | Значение | Назначение |
|---|---|---|
sourceId |
budget-process-registry |
Папка документации |
parentSlug |
budget-process-registry |
Source card/detail/refresh |
routeSlug |
budget-process-registry |
Frontend route |
parserSource |
budget_ubpandnubp |
Dashboard/run/jobs/logs |
sourceGroup |
budget_process_registry |
Records/export enum |
recordType |
budget_registry_organization |
Registry record/version |
sourceItemCode |
budget_ubpandnubp |
Единственный source item |
refreshKey |
budget_ubpandnubp |
Refresh/schedule key |
taskName |
parsers.budget_ubpandnubp.refresh |
Background task metadata |
tableKey |
budget-process-registry-records |
Только frontend |
Backend принимает и возвращает только канонические значения из таблицы. Frontend resolver
может распознавать budget-process-registry, budget_process_registry и
budget_ubpandnubp, но не отправляет их как взаимозаменяемые API-параметры. Backend не
возвращает tableKey. uid записи и organization.uid — разные стабильные UUID.
external_id равен upstream id. Уникальность published record: (source, external_id).
info.recordNum, info.guid и info.parentrecordnum хранятся отдельно и не заменяют
external_id.
4. Обязательный контракт организации
| Пользовательское поле | API-поле | Тип | Required | Nullable | Upstream / правило |
|---|---|---|---|---|---|
| Наименование | organization.name |
string | Да | Нет | fullName, fallback shortName, enrichment |
| ИНН | organization.inn |
string | Да | Нет | inn; leading zeros сохраняются |
| ОГРН | organization.ogrn |
string | Да | Нет | ogrn; leading zeros сохраняются |
| ОКПО | organization.okpo |
string | Да | Нет | okpoCode; enrichment при отсутствии |
| КПП | organization.kpp |
string | Да | Да | kpp |
| Полное название | organization.full_name |
string | Да | Да | fullName |
| Короткое название | organization.short_name |
string | Да | Да | shortName |
| Юридический адрес | organization.legal_address |
string | Да | Да | Собран из структурных частей |
| Обособленное подразделение | organization.is_branch |
boolean | Да | Да | Строгая нормализация isObosob |
Минимальный объект:
{
"organization": {
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
"name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"full_name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"short_name": "МОУ",
"inn": "1622003200",
"kpp": "162201001",
"ogrn": "1021605955777",
"okpo": "54444331",
"legal_address": "422838, Республика Татарстан, Камско-Устьинский район, деревня Малые Кармалы",
"is_branch": false
}
}
Алгоритм linkage: точный ОГРН → точный ИНН+КПП → однозначный enrichment. Конфликтующие
совпадения не выбираются автоматически. Отсутствие обязательного квартета создаёт quarantine
record с machine-readable reason; "", "—", 0 и копирование другого кода запрещены.
5. Матрица endpoint и решение по реализации
| № | Метод и endpoint | Статус | Изменение | Потребитель |
|---|---|---|---|---|
| 1 | GET /api/v1/sources/ |
Нужно расширить | Карточка нового slug | /sources, /main |
| 2 | GET /api/v1/sources/{slug}/ |
Нужно расширить | Source item/latest load/meta | Source detail/scraping |
| 3 | GET /api/v1/parsers/dashboard/ |
Нужно расширить | Typed source/count/schedule/coverage | Main/detail/scraping |
| 4 | GET /api/v2/organization-source-records/ |
Нужно расширить | Enums, filters, list payload | Source/org tables |
| 5 | GET /api/v2/organization-source-records/{uid}/ |
Нужно расширить | Full detail payload | Record detail |
| 6 | POST /api/v1/sources/{slug}/refresh/ |
Нужно расширить | New slug, typed task_ids | Scraping |
| 7 | POST /api/v1/parsers/run/{source_key}/ |
Нужно расширить | New source, typed response | Admin direct run |
| 8 | GET /api/v1/jobs/{task_id}/ |
Существует, нужно типизировать | Status/result schema | Polling |
| 9 | GET /api/v1/system/logs/ |
Нужно расширить | Source enum/filter/meta | History |
| 10 | GET /api/v1/system/logs/{id}/ |
Нужно расширить | Source label/result meta | History detail |
| 11 | GET /api/v1/system/logs/export/ |
Нужно расширить | Source enum/filter | History CSV |
| 12 | POST /api/v2/organization-source-records/export-ticket/ |
Нужно описать в OpenAPI | New source group | Export |
| 13 | POST /api/v2/organization-source-records/export-download/ |
Нужно описать в OpenAPI | Ticket download | Export |
Существующие, но не обязательные endpoint:
| Endpoint | Решение |
|---|---|
GET /api/v1/sources/statuses/ |
Не требуется: active GET callsite отсутствует |
POST /api/v2/organization-source-records/export/ |
Не canonical; ticket flow выбран |
GET /api/v1/parsers/sources/ |
Не требуется: metadata приходит dashboard/source detail |
GET/POST /api/v1/parsers/schedules/* |
Не требуется frontend: расписание приходит dashboard |
GET /api/v1/parsers/load-logs/ |
Не требуется: история использует /system/logs/ |
GET /api/v1/parsers/records/ |
Не требуется: таблица использует v2 records |
GET /api/v1/jobs/ |
Не требуется: polling по task ID |
POST /api/v1/jobs/{task_id}/control/ |
Не требуется в v1: cancel UI не заявлен |
GET /api/v1/jobs/{task_id}/stream/ |
Не требуется: polling 10 секунд |
POST /api/v1/parsers/upload/{source_key}/ |
Не требуется: источник получает backend по API |
GET /api/v2/sources/<legacy-source>/.../download/ |
Не требуется: общий export |
6. Общий паттерн описания endpoint
Форматы: datetime ISO 8601 с timezone; date YYYY-MM-DD; counters integer >=0; UUID string;
пустые коллекции []; unknown nullable только при явном nullable: true.
Budget datetime нормализуется в UTC RFC3339 с суффиксом Z. Если upstream datetime
не содержит timezone, приложение интерпретирует его как UTC по принятому операционному
правилу; это не утверждение о timezone самого Budget API. Aware datetime переводится
в UTC, а исходная строка сохраняется в raw payload. Обычные date-поля остаются датами.
Каноническая бизнес-ошибка:
{
"success": false,
"errors": [
{
"code": "validation_error",
"message": "Некорректный параметр source.",
"field": "source"
}
],
"meta": {
"request_id": "7ba5ed18-29e2-4d85-b693-928ffc41f5ee"
},
"data": null
}
Auth middleware может вернуть только отдельно описанный ответ:
{
"detail": "Учетные данные не были предоставлены."
}
Матрица общих ошибок:
| HTTP | Code | Условие | Retry |
|---|---|---|---|
| 400 | validation_error |
Query/body/UUID invalid | После исправления |
| 401 | auth middleware | Нет/истёк token | После входа/refresh token |
| 403 | permission_denied |
Нет роли | Нет |
| 404 | not_found |
Slug/record/task/log отсутствует | Нет |
| 409 | refresh_already_running |
Duplicate active run | После terminal |
| 410 | ticket_expired |
Export ticket expired/used | Создать новый ticket |
| 413 | export_too_large |
Превышен export limit | Сузить scope/async policy |
| 429 | rate_limited |
Backend rate limit | По Retry-After |
| 500 | internal_error |
Необработанная ошибка | По retryable flag |
7. Каталог источников
7.1 GET /api/v1/sources/
Request:
GET /api/v1/sources/
Authorization: Bearer <token>
Response 200 application/json содержит среди data:
{
"success": true,
"errors": null,
"meta": {},
"data": [
{
"slug": "budget-process-registry",
"title": "Реестр участников бюджетного процесса",
"description": "Паспорт, бюджетная принадлежность, полномочия, счета и связи организаций.",
"order": 130,
"is_available": true,
"status": "success",
"status_label": "Обновлено",
"progress": 100,
"records_count": 1200,
"organizations_count": 900,
"last_updated_at": "2026-08-24T03:45:00+03:00",
"next_update_at": "2026-08-25T03:00:00+03:00",
"error_message": "",
"task_names": ["parsers.budget_ubpandnubp.refresh"],
"refresh_requires_params": false,
"refresh_params": []
}
]
}
records_count — published versions; organizations_count — distinct organization.uid;
quarantine не включается. last_updated_at — публикация последнего successful snapshot.
Sorting: order ASC, slug ASC. Errors: общие 401, 429, 500 из раздела 6.
7.2 GET /api/v1/sources/{slug}/
Request:
GET /api/v1/sources/budget-process-registry/
Authorization: Bearer <token>
Response 200 application/json:
{
"success": true,
"errors": null,
"meta": {},
"data": {
"slug": "budget-process-registry",
"title": "Реестр участников бюджетного процесса",
"description": "Паспорт, бюджетная принадлежность, полномочия, счета и связи организаций.",
"order": 130,
"is_available": true,
"status": "success",
"status_label": "Обновлено",
"progress": 100,
"records_count": 1200,
"organizations_count": 900,
"last_updated_at": "2026-08-24T03:45:00+03:00",
"next_update_at": "2026-08-25T03:00:00+03:00",
"error_message": "",
"task_names": ["parsers.budget_ubpandnubp.refresh"],
"refresh_requires_params": false,
"refresh_params": [],
"active_tasks": [],
"source_items": [
{
"code": "budget_ubpandnubp",
"refresh_key": "budget_ubpandnubp",
"title": "Реестр участников бюджетного процесса",
"description": "Нормализованные записи публичного реестра Budget.gov.",
"parser_source": "budget_ubpandnubp",
"parser_source_display": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"last_updated_at": "2026-08-24T03:45:00+03:00",
"latest_load": {
"batch_id": 8201,
"source": "budget_ubpandnubp",
"source_label": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"status": "success",
"error_message": "",
"created_at": "2026-08-24T03:00:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00",
"meta": {
"source_version": "10",
"raw_records_count": 1229,
"published_records_count": 1200,
"active_records_count": 800,
"quarantined_records_count": 29,
"pages_count": 2
}
},
"latest_success_load": {
"batch_id": 8201,
"source": "budget_ubpandnubp",
"source_label": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"status": "success",
"error_message": "",
"created_at": "2026-08-24T03:00:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00",
"meta": {
"source_version": "10",
"raw_records_count": 1229,
"published_records_count": 1200,
"active_records_count": 800,
"quarantined_records_count": 29,
"pages_count": 2
}
}
}
],
"latest_load": null,
"latest_success_load": null
}
}
latest_load отличается от latest_success_load: failed batch не заменяет successful.
Неизвестный slug → 404 not_found. active_tasks[], когда непустой, имеет required
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 application/json внутри полного named ParserDashboardResponse:
{
"success": true,
"data": {
"sources": [
{
"key": "budget_ubpandnubp",
"source": "budget_ubpandnubp",
"title": "Реестр участников бюджетного процесса",
"agency": "Единый портал бюджетной системы РФ",
"data_scope": "Участники бюджетного процесса и иные юридические лица",
"task_name": "parsers.budget_ubpandnubp.refresh",
"is_existing": true,
"requires_file_url": false,
"mode": "scheduled",
"status": "active",
"owner": "backend-mostovik",
"upstream_url": "https://budget.gov.ru/epbs/registry/ubpandnubp/data",
"access_method": "api",
"parser_strategy": "full_snapshot",
"source_notes": "Frontend не обращается к upstream.",
"supports_file_upload": false,
"api_route": "/api/v1/parsers/run/budget_ubpandnubp/",
"result_list_url": "/api/v2/organization-source-records/?source=budget_ubpandnubp",
"result_detail_url": "/api/v2/organization-source-records/{uid}/",
"upload_url": ""
}
],
"source_counts": {
"budget_ubpandnubp": 1200
},
"load_logs": [],
"schedules": [
{
"id": 913,
"key": "budget_ubpandnubp",
"name": "parsers.budget_ubpandnubp.refresh",
"title": "Реестр участников бюджетного процесса",
"source": "budget_ubpandnubp",
"source_key": "budget_ubpandnubp",
"enabled": true,
"schedule_type": "crontab",
"schedule": {
"minute": "0",
"hour": "3",
"day_of_week": "*",
"day_of_month": "*",
"month_of_year": "*"
}
}
],
"registry_enrichment_analytics": {
"population": {
"active_registry_organizations": 1200
},
"coverage_summary": {
"with_any_enrichment": 1150,
"core_profile_complete": 1100,
"requires_attention": 50
},
"source_coverage": [
{
"source": "budget_ubpandnubp",
"label": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"coverage_percent": 75.0,
"last_updated_at": "2026-08-24T03:45:00+03:00"
}
],
"risk_signals": []
}
}
}
source_coverage[].organizations_count — число организаций ОПК, покрытых источником, и не
равно /sources[].organizations_count, считающему все уникальные организации источника.
coverage_percent = organizations_count / population.active_registry_organizations * 100;
при нулевой population — 0. Risk signal для источника не создаётся. Все dashboard blocks
формируются из одного snapshot. Errors/permissions: 401, 403, 429, 500.
9. Записи источника
9.1 GET /api/v2/organization-source-records/
Request:
GET /api/v2/organization-source-records/?source_group=budget_process_registry&source=budget_ubpandnubp&record_type=budget_registry_organization&page=1&page_size=50&ordering=extension__organization__name
Authorization: Bearer <token>
Query:
| Параметр | Тип | Required | Default/limit | Правило |
|---|---|---|---|---|
source_group |
enum | Да для source page | — | budget_process_registry |
source |
enum/string | Да | — | budget_ubpandnubp |
record_type |
enum/string | Да | — | budget_registry_organization |
status |
enum | Нет | — | active,inactive,special,unknown |
organization |
UUID | Нет | — | Точный organization.uid |
search |
string | Нет | "", max 200 |
Имя, ИНН, КПП, ОГРН, ОКПО, code, regNum, recordNum |
date_from/date_to |
date | Нет | — | Включительно по record_date |
region_code |
string | Нет | — | payload.address.region.code |
organization_type |
string | Нет | — | Код типа организации |
establishment_kind |
string | Нет | — | Код вида учреждения |
budget_level |
string | Нет | — | Код уровня бюджета |
is_branch |
boolean | Нет | — | Обособленное подразделение |
has_procurement_permission |
boolean | Нет | — | Summary flag |
ordering |
enum | Нет | extension__organization__name |
Whitelist ниже |
page |
integer | Нет | 1, min 1 | Страница |
page_size |
integer | Нет | 50, max 100 | Размер страницы |
Ordering whitelist с - variant: extension__organization__name, status,
payload__classification__organization_type__name,
payload__classification__establishment_kind__name, payload__budget__level__name,
payload__address__region__name, updated_at, payload__is_separate_division.
Stable tie-breaker — uid; nulls last. Идентификаторы ищутся через search; для статуса,
классификации, региона и признака филиала backend поддерживает и фильтрацию, и сортировку.
Response 200 application/json:
{
"success": true,
"data": [
{
"uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
"extension_uid": "3e4c092e-81fb-4d41-9acb-f77922724339",
"source_group": "budget_process_registry",
"record_type": "budget_registry_organization",
"source": "budget_ubpandnubp",
"external_id": "3320010",
"title": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"record_date": "2025-11-13",
"amount": null,
"status": "inactive",
"url": "https://budget.gov.ru/epbs/registry/ubpandnubp/data?filterid=3320010",
"payload": {
"registry": {
"code": "923J0795",
"registration_number": "J1516",
"record_number": "926300000332J0795012"
},
"classification": {
"organization_type": { "code": "03", "name": "Учреждение" },
"establishment_kind": { "code": "2", "name": "Бюджетное" }
},
"budget": {
"level": { "code": "32", "name": "Бюджет муниципального района" },
"code": "11031094",
"name": "Бюджет Камско-Устьинского муниципального района"
},
"address": {
"region": { "code": "16", "name": "ТАТАРСТАН" }
},
"is_separate_division": false,
"summary": {
"activities_count": 1,
"authorities_count": 1,
"permissions_count": 3,
"accounts_count": 1,
"successions_count": 0,
"has_procurement_permission": true
}
},
"legacy_model": "",
"legacy_pk": "",
"load_batch": 8201,
"created_at": "2026-08-24T03:40:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00",
"financial_lines": [],
"organization": {
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
"name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"full_name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"short_name": "МОУ",
"inn": "1622003200",
"kpp": "162201001",
"ogrn": "1021605955777",
"okpo": "54444331",
"legal_address": "422838, Республика Татарстан, Камско-Устьинский район, деревня Малые Кармалы",
"is_branch": false
}
}
],
"errors": null,
"meta": {
"pagination": {
"page": 1,
"page_size": 50,
"total_count": 800,
"total_pages": 4200,
"has_next": true,
"has_previous": false
}
}
}
List payload schema:
| JSON path | Тип | Required | Nullable | Upstream/правило |
|---|---|---|---|---|
payload.registry.code |
string | Да | Нет | info.code |
payload.registry.registration_number |
string | Да | Да | info.regNum |
payload.registry.record_number |
string | Да | Да | info.recordNum |
payload.classification.organization_type |
CodeName | Да | Да | orgTypeCode/Name |
payload.classification.establishment_kind |
CodeName | Да | Да | establishmentKindCode/Name |
payload.budget.level |
CodeName | Да | Да | budgetLvlCode/Name |
payload.budget.code |
string | Да | Да | budgetCode |
payload.budget.name |
string | Да | Да | budgetName |
payload.address.region |
CodeName | Да | Да | regionCode/Name |
payload.is_separate_division |
boolean | Да | Да | isObosob |
payload.summary.*_count |
integer | Да | Нет | Count detail collections |
payload.summary.has_procurement_permission |
boolean | Да | Нет | Nonempty active procurement role |
Errors: 400 invalid filter/date/order/page, 401, 403, 429, 500 по разделу 6.
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 application/json — plain record без envelope, общие поля идентичны list;
payload заменяется BudgetRegistryRecordDetailPayload:
{
"uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
"extension_uid": "3e4c092e-81fb-4d41-9acb-f77922724339",
"source_group": "budget_process_registry",
"record_type": "budget_registry_organization",
"source": "budget_ubpandnubp",
"external_id": "3320010",
"title": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"record_date": "2025-11-13",
"amount": null,
"status": "inactive",
"url": "https://budget.gov.ru/epbs/registry/ubpandnubp/data?filterid=3320010",
"payload": {
"registry": {
"code": "923J0795",
"registration_number": "J1516",
"record_number": "926300000332J0795012",
"guid": "6152B151-D365-42F6-A8D7-C6FF25D19E2B",
"parent_record_number": "926300000332J0795011",
"status_code": "2",
"status_name": "недействующая",
"registration_date": "2017-12-30",
"inclusion_date": "2015-12-23T17:58:53Z",
"exclusion_date": null,
"start_date": "2019-09-03T17:59:53Z",
"end_date": null,
"updated_at": "2025-11-13T18:44:39Z"
},
"legal": {
"firm_name": null,
"name_in_documents": null,
"legal_form": { "code": "75403", "name": "Муниципальные бюджетные учреждения" },
"ownership_form": { "code": "14", "name": "Муниципальная собственность" },
"legal_person_kind": { "code": "01", "name": "Создание юридического лица до 01.07.2002" }
},
"classification": {
"organization_type": { "code": "03", "name": "Учреждение" },
"establishment_kind": { "code": "2", "name": "Бюджетное" },
"government_body": null,
"flags": {
"is_government_body": false,
"is_separate_division": false,
"is_institution": false,
"is_reorganized": false,
"is_excluded": false,
"not_in_egrul": false,
"contour_type_code": "O"
}
},
"budget": {
"level": { "code": "32", "name": "Бюджет муниципального района" },
"code": "11031094",
"name": "Бюджет Камско-Устьинского муниципального района",
"public_legal_entity": { "code": "32", "name": "Муниципальный район" },
"budget_chapter": { "code": "508", "name": "Управление образования" },
"authorized_organization": { "code": "92300018", "name": "Финансово-бюджетная палата" },
"treasury_body": { "code": "1100", "name": "УФК по Республике Татарстан" }
},
"address": {
"full": "422838, Республика Татарстан, Камско-Устьинский район, деревня Малые Кармалы",
"postal_code": "422838",
"country": null,
"region": { "code": "16", "name": "ТАТАРСТАН" },
"district": { "code": "1602300000000", "type": "РАЙОН", "name": "КАМСКО-УСТЬИНСКИЙ" },
"city": null,
"locality": { "code": "1602300004000", "type": "ДЕРЕВНЯ", "name": "МАЛЫЕ КАРМАЛЫ" },
"street": null,
"house": "-",
"building": null,
"apartment": null,
"oktmo": { "code": "92630470106", "name": "д Малые Кармалы" },
"okato": null,
"kladr_code": "1600000000000"
},
"hierarchy": {
"founder_kind": { "code": "32", "name": "Муниципальный район" },
"founder_place": { "code": "92630000", "name": "Камско-Устьинский муниципальный район" },
"creator_kind": null,
"creator_place": null,
"parent_organization": null,
"division_parent": null
},
"reorganization": {
"code": null,
"name": null,
"document": null,
"document_number": null,
"document_date": null,
"start_date": null,
"end_date": "2019-04-26"
},
"upstream_audit": {
"source_version": "10",
"load_date": "2025-11-13T22:48:36Z",
"first_registration_guid": "909f2e70-1ed4-46d2-aba2-c84f29fc651b",
"last_registration_guid": "3162836d-438c-42e6-ade4-3b2cab991037",
"last_registration_number": "1-76-16-000/17995",
"control_number": "0",
"bid_number": "1-76-16-000/00202",
"update_number": "1",
"update_reason": "E"
},
"heads": [
{
"full_name": "ГИМАДЕЕВА ЕЛЕНА АЛЕКСАНДРОВНА",
"position": "ЛИКВИДАТОР",
"is_primary": false,
"document_name": null,
"document_number": null,
"document_date": null
}
],
"contacts": [{ "phone": "8 843 772 14 05", "email": "cbkamust@mail.ru", "website": null }],
"activities": [{ "code": "85.12", "name": "Образование начальное общее", "kind": "основной" }],
"authorities": [
{
"code": "92303386",
"name": "ИСПОЛНИТЕЛЬНЫЙ КОМИТЕТ",
"permissions": [
{ "code": "403", "name": "Назначение руководителя" },
{ "code": "401", "name": "Выполнение функций учредителя" }
]
}
],
"permissions": {
"participant": [],
"non_participant": [],
"procurement": [
{ "code": "201", "name": "заказчик", "start_date": "2016-07-11", "end_date": null }
],
"accepted": [],
"transferred": [],
"budget_participant": [],
"budget_institution": []
},
"accounts": {
"personal": [],
"financial_authority": [
{
"number": "22508032",
"type_name": "ЛБО",
"authority_code": "92300018",
"authority_name": "Финансово-бюджетная палата"
}
],
"treasury": []
},
"successions": [],
"contracts": [],
"attachments": [],
"unclassified_blocks": {}
},
"legacy_model": "",
"legacy_pk": "",
"load_batch": 8201,
"created_at": "2026-08-24T03:40:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00",
"financial_lines": [],
"organization": {
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
"name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"full_name": "МУНИЦИПАЛЬНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ",
"short_name": "МОУ",
"inn": "1622003200",
"kpp": "162201001",
"ogrn": "1021605955777",
"okpo": "54444331",
"legal_address": "422838, Республика Татарстан, Камско-Устьинский район, деревня Малые Кармалы",
"is_branch": false
}
}
Detail named collections and upstream mapping:
| API path | Поля элемента / upstream block |
|---|---|
heads[] |
full_name,position,is_primary,document_name,document_number,document_date ← heads.fio,post,headMain,docName,docNum,docDate |
contacts[] |
phone,email,website ← contacts.phone,mail,site |
activities[] |
code,name,kind ← activities.activityCode,activityName,activityKind |
authorities[] |
code,name,permissions[]{code,name} ← authorities |
successions[] |
source,parent_code,parent_name,ogrn,document_name,document_number,document_date ← successions |
accounts.personal[] |
number,type,status,open_date,close_date,open_treasury,service_treasury,account_organization,public_legal_entity ← facialAccounts |
accounts.financial_authority[] |
number,type_name,authority_code,authority_name ← foAccounts |
accounts.treasury[] |
number,type_name,open_date,close_date,open_treasury,public_legal_entity,service_treasury_ref ← ksaccounts |
permissions.participant[] |
code,name,start_date,end_date ← participantPermissions |
permissions.non_participant[] |
base fields + registry_number,budget,public_legal_entity,budget_chapter ← nonParticipantPermissions |
permissions.procurement[] |
code,name,start_date,end_date ← procurementPermissions |
permissions.accepted[] |
base + budget,public_legal_entity,budget_chapter,giver,user_area,registry_number ← acceptAuths |
permissions.transferred[] |
registry_number,start_date,end_date,budget_code,budget_chapter_code,municipalities ← transfauth |
permissions.budget_participant[] |
budget,budget_level,public_legal_entity,budget_chapter_code,budget_chapter_name ← ubptransfauthbp |
permissions.budget_institution[] |
Пока без named element schema: непустой ubptransfauthbu временно входит в unclassified_blocks |
contracts[] |
number,sign_date,organization_code,organization_name,budget_code ← contracts |
attachments[] |
Named attachment schema после непустого sample; до этого raw item также остаётся в unclassified_blocks |
attachment, ubptransfauthbu, ubpfin не имели непустого runtime sample на исследованных
страницах. Backend не выбрасывает их: сохраняет raw, пишет schema-drift metric и отдаёт
непустые значения в unclassified_blocks: Record<string, JsonObject[]>. Frontend показывает
generic key/value section только при наличии. После получения sample backend добавляет named
schema без удаления raw lineage.
Все 128 upstream info keys распределяются по registry, legal, classification,
budget, address, hierarchy, reorganization, upstream_audit; полный исходный объект
хранится backend, но admin raw JSON загружается из уже полученного detail DTO/export, а не из
Budget.gov.
Detail errors: invalid UUID 400, missing 404, auth 401/403, 429, 500.
10. Refresh и фоновые задачи
10.1 POST /api/v1/sources/{slug}/refresh/
Request:
POST /api/v1/sources/budget-process-registry/refresh/
Authorization: Bearer <admin-token>
Content-Type: application/json
{"params":{}}
Response 202 application/json:
{
"status": "queued",
"task_ids": ["cce43750-3c50-48b7-aade-22cf4eb6cf87"]
}
Параметры upstream URL/page size frontend не передаёт. Duplicate active run → 409. Дедупликация: один active orchestration task на parser source; request idempotency key может дополнительно связывать повтор сети, но не является пользовательским параметром.
10.2 POST /api/v1/parsers/run/{source_key}/
Применим для прямого административного запуска того же parser.
Request:
POST /api/v1/parsers/run/budget_ubpandnubp/
Authorization: Bearer <admin-token>
Response 201 application/json:
{
"task_id": "cce43750-3c50-48b7-aade-22cf4eb6cf87",
"status": "queued",
"source": "budget_ubpandnubp"
}
Errors: 400, 401, 403, 404, 409, 429, 500. Task доступна через job detail.
10.3 GET /api/v1/jobs/{task_id}/
Request:
GET /api/v1/jobs/cce43750-3c50-48b7-aade-22cf4eb6cf87/
Authorization: Bearer <token>
Response 200 application/json:
{
"task_id": "cce43750-3c50-48b7-aade-22cf4eb6cf87",
"status": "in_progress",
"progress": 42,
"message": "Получено 149 из 353 страниц",
"result": {
"batch_id": 8201,
"pages_completed": 149,
"pages_total": 353,
"raw_records_count": 149000,
"published_records_count": 0,
"quarantined_records_count": 0,
"snapshot_published": false
},
"error": ""
}
Status enum: queued,in_progress,retry,success,failed,cancelled,skipped. Terminal:
success,failed,cancelled,skipped. Progress monotonic 0..100; success = 100. Result schema
required, поля nullable до известности. Owner/admin access; unknown 404; foreign task 403;
retention не менее 7 дней после terminal.
10.4 POST /api/v1/parsers/upload/{source_key}/ (условный)
Не требуется: источник API-based, backend сам получает данные. Frontend request не отправляет.
Если общий route вызван ошибочно:
POST /api/v1/parsers/upload/budget_ubpandnubp/
Authorization: Bearer <admin-token>
Response 405 application/json:
{
"success": false,
"errors": [
{
"code": "upload_not_supported",
"message": "Источник budget_ubpandnubp не поддерживает загрузку файла.",
"field": null
}
],
"meta": {},
"data": null
}
11. История обновлений
11.1 GET /api/v1/system/logs/
Request:
GET /api/v1/system/logs/?source=budget_ubpandnubp&ordering=-updated_at&page=1&page_size=100
Authorization: Bearer <admin-token>
Response 200 application/json:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 8201,
"batch_id": 8201,
"source": "budget_ubpandnubp",
"source_label": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"status": "success",
"status_label": "Успешно",
"error_message": "",
"created_at": "2026-08-24T03:00:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00"
}
]
}
Filters: source,status,batch_id,search,ordering,page,page_size. Search: batch ID, source label,
error message. Status transitions: pending → in_progress → success|failed|skipped; cancelled
из active. Только admin. Errors: 401, 403, 429, 500.
11.2 GET /api/v1/system/logs/{id}/
Request:
GET /api/v1/system/logs/8201/
Authorization: Bearer <admin-token>
Response 200 application/json:
{
"id": 8201,
"batch_id": 8201,
"source": "budget_ubpandnubp",
"source_label": "Реестр участников бюджетного процесса",
"records_count": 1200,
"organizations_count": 900,
"status": "success",
"error_message": "",
"created_at": "2026-08-24T03:00:00+03:00",
"updated_at": "2026-08-24T03:45:00+03:00",
"meta": {
"job_id": "cce43750-3c50-48b7-aade-22cf4eb6cf87",
"source_version": "10",
"raw_records_count": 1229,
"published_records_count": 1200,
"active_records_count": 800,
"quarantined_records_count": 29,
"pages_count": 2,
"snapshot_published": true
}
}
List и detail используют единое source_label; source_display deprecated. job_id связывает
log с job. Errors: 401, 403, 404, 429, 500.
11.3 GET /api/v1/system/logs/export/
Request:
GET /api/v1/system/logs/export/?source=budget_ubpandnubp&ordering=-updated_at
Authorization: Bearer <admin-token>
Response 200:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="update-history-2026-08-24_03-45.csv"
CSV UTF-8 BOM, ;, CRLF; columns: ID, Batch ID, Источник, Статус, Записи, Организации,
Ошибка, Создано, Обновлено. Filters/order совпадают с list, пагинация не применяется.
12. Выгрузка данных источника
Canonical flow — ticket. Export содержит опубликованные записи, common fields, полный detail
payload и organization. Для CSV/XLSX nested collections раскладываются в отдельные файлы/листы
с record_uid foreign key; JSON сохраняет вложенность.
12.1 POST /api/v2/organization-source-records/export-ticket/
Request application/json:
{
"sources": ["budget_process_registry"],
"format": "xlsx"
}
Response 201 application/json:
{
"ticket": "opaque-single-use-token",
"expires_in": 60,
"file_name": "organization_source_records_export.zip"
}
Admin-only. Ticket opaque, user-bound, single-use, TTL 60 seconds. Errors: 400, 401, 403,
409, 413, 429, 500.
12.2 POST /api/v2/organization-source-records/export-download/
Request:
POST /api/v2/organization-source-records/export-download/
Content-Type: application/x-www-form-urlencoded
ticket=opaque-single-use-token
Response 200:
Content-Type: application/zip
Content-Disposition: attachment; filename="organization_source_records_export.zip"
Invalid 400, missing 404, expired/used 410, permission 403, too large 413, internal 500.
12.3 POST /api/v2/organization-source-records/export/
Не canonical и frontend request не отправляет. До удаления endpoint может принять:
{
"sources": ["budget_process_registry"],
"format": "xlsx"
}
Response не используется frontend; OpenAPI помечает operation deprecated и указывает ticket flow. Одновременно поддерживать два canonical поведения запрещено.
Форматы: csv,xlsx,json. ZIP entry prefix: budget-process-registry/. JSON schema равна
detail schema; CSV UTF-8 BOM; порядок external_id ASC, uid ASC. Максимальный sync generated
archive — 2 GiB; при превышении ticket job создаёт chunked ZIP или возвращает 413 до download.
13. Статусы и жизненный цикл
| Категория | Raw statuses | UI label |
|---|---|---|
| Выполняется | queued,in_progress,retry |
Выполняется |
| Terminal | success,failed,cancelled,skipped |
Обновлено |
История показывает точный исход. Допустимые переходы:
queued → in_progress → success
→ failed
→ cancelled
retry ─────┘
queued/in_progress → skipped при подтверждённом неизменившемся snapshot
Retry не создаёт новый published batch. Повторный ручной запуск после terminal создаёт новый job/batch. UI timeout не отменяет backend task.
14. Согласованность данных между endpoint
| Данные | Источник истины | Инвариант |
|---|---|---|
parentSlug |
Source registration | budget-process-registry в list/detail/refresh |
parserSource |
Parser registration | budget_ubpandnubp в dashboard/records/logs/run |
sourceGroup |
Record extension enum | budget_process_registry в records/export |
| Последняя успешная дата | Successful published batch | Одинакова в card/dashboard/latest success |
| Records count | Published snapshot | Одинаковая семантика versions во всех endpoint |
| Organizations count | Distinct organization.uid |
Source card count; coverage считает только ОПК intersection |
| Load batch | ParserLoadLog | records.load_batch = logs.batch_id |
| Job | Queue task | Refresh task IDs читаются job endpoint |
Publication atomicity: staging records и relations становятся current snapshot одной транзакцией/атомарным pointer switch. Частичный batch никогда не участвует в list/count/export.
15. Ошибки, доступ, производительность и ограничения
Матрица доступа
| Операция | Authenticated | Admin | Permission |
|---|---|---|---|
| Read source cards/dashboard | Да | Да | Стандартный read |
| Read records/detail | Да | Да | Стандартный read; account fields могут требовать отдельный permission по решению ИБ |
| Refresh/parser run | Нет | Да | sources.refresh |
| Jobs | Владелец task | Да | jobs.read |
| Logs/export | Нет | Да | system_logs.read, sources.export |
Нефункциональные требования
| Требование | Значение |
|---|---|
| P95 sources list | ≤ 500 ms без upstream call |
| P95 dashboard | ≤ 1 s без upstream call |
| P95 records list | ≤ 1 s при page_size 50 |
| P95 record detail | ≤ 2 s для 95% записей |
| Максимальный page_size internal API | 100 |
| Rate limit read | 120 req/min/user |
| Rate limit refresh | 2 req/min/admin, duplicate 409 |
| Cache/ETag | Source/detail list может иметь ETag 60 s; records current snapshot cache 60 s |
| Timeout upstream | Connect 10 s, response page 60 s |
| Retry upstream | 5 attempts exponential backoff+jitter; 429 respects Retry-After |
| Schema drift | Не публиковать потерянные поля; raw + alert + quarantine/known generic block |
| Logging | Не логировать полные account/contact payloads; secrets/stack traces запрещены |
List/dashboard не выполняют N+1 и никогда не вызывают Budget.gov синхронно.
16. Требования к OpenAPI и generated-клиенту
- добавить
budget_ubpandnubpв parser/log source enums; - добавить
budget_process_registryв source group/export enums; - добавить
budget_registry_organizationrecord type; - required common record fields и organization name/inn/ogrn/okpo;
- именованные
BudgetRegistryRecordListPayloadиBudgetRegistryRecordDetailPayload; - named schemas для всех nested collections и
CodeName; - discriminator по
source + record_typeлибо документированныйoneOf; - named
OrganizationSourceRecordPaginationвместо unknownmeta; - typed dashboard/parser-run/job/log-meta/export ticket/download;
- все success/error responses и MIME types;
- unique stable operationId, включая detail identifier;
- ticket/download paths присутствуют в OpenAPI;
- examples валидируются schema и не заменяют required/nullable.
Frontend после публикации выполняет:
bun run apigen
bun run type-check
bun run test:contract
Проверяются src/shared/api/generated-api/ и src/shared/model/generated-zod/. Generated files
не редактируются вручную. Новый ручной adapter не остаётся после готовности схемы.
17. Backend-тесты и acceptance criteria
Минимальная матрица backend/contract tests
| Сценарий | Проверка |
|---|---|
| Upstream pagination | 1, 1000, last page; изменение recordCount; retry 429/5xx |
| Blocks | Все 18 blocks сохраняются; unknown nonempty не теряется |
| Snapshot | Partial/failure не публикуется; successful switch atomic |
| Organization | Required name/inn/ogrn/okpo; enrichment/quarantine conflicts |
| Identity/history | Stable uid; external id; historical versions не схлопнуты |
| Filters/search | Все query из 9.1 работают совместно |
| Ordering | Whitelist, reverse, tie-breaker, nulls last |
| Pagination | Empty/first/last total/pages/flags |
| Detail | List common fields равны detail; all collections typed |
| Refresh/jobs | task IDs, progress, duplicate 409, active→terminal |
| Dashboard/cards/logs | Identifiers, counts, timestamps и batch согласованы |
| Export | CSV/XLSX/JSON, nested relations, permissions/ticket TTL |
| OpenAPI | Runtime examples валидируются generated schema |
Backend acceptance criteria
- Все строки endpoint matrix реализованы согласно статусу.
- Backend сам получает и проверяет полный snapshot Budget.gov.
- Каждая published запись содержит Наименование, ИНН, ОГРН и ОКПО.
- List payload лёгкий; detail payload полный и типизированный.
- Все 18 upstream blocks сохраняются без silent data loss.
- Dashboard/cards/jobs/logs согласованы по identifiers/counts/time/status.
- Ticket flow является единственным canonical export flow.
- Error schemas, permissions, limits и MIME протестированы.
- OpenAPI не содержит void/unknown object для используемых responses.
bun run apigenсоздаёт пригодные DTO без ручного дублирования.- Contract tests проходят на целевом backend.
18. Чего backend не должен возвращать
- HTML, Vue components, CSS classes, icons/colors;
- форматированные числа, даты и UI placeholders;
- account/contact secrets или internal exception details;
- raw upstream как единственный payload;
- большие detail collections в list;
tableKeyи route names;- разные значения identifiers/count semantics в разных endpoint;
- source-specific поля вне OpenAPI;
- синхронный live proxy Budget.gov при открытии записи frontend;
- потерянные неизвестные upstream blocks.
19. Решения и открытые вопросы
Документ остаётся review до закрытия решений.
| ID | Вопрос / решение | Ответственный | Срок | Статус | Результат |
|---|---|---|---|---|---|
BE-budget-process-registry-001 |
Права на account sections | Product/ИБ | До contract freeze | Открыт | По умолчанию authenticated; подтвердить отдельный permission |
BE-budget-process-registry-002 |
Retention raw snapshots | Backend/ИБ | До production rollout | Предложение | ≥90 дней и ≥10 snapshots |
BE-budget-process-registry-003 |
Непустая schema attachment/ubpfin blocks | Backend/аналитик | При первом sample | Контролируется | Raw generic block + schema-drift alert, затем named schema |
BE-budget-process-registry-004 |
Допустимый объём export | Backend/Product | До нагрузочного теста | Открыт | Ticket archive с лимитом 2 GiB или chunking |