Files
mostovik-backend/docs/source-integration/sources/budget-process-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

70 KiB
Raw Blame History

template_id, template_version, source_id, source_name, document_status
template_id template_version source_id source_name document_status
source-backend-api 1 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_dateheads.fio,post,headMain,docName,docNum,docDate
contacts[] phone,email,websitecontacts.phone,mail,site
activities[] code,name,kindactivities.activityCode,activityName,activityKind
authorities[] code,name,permissions[]{code,name}authorities
successions[] source,parent_code,parent_name,ogrn,document_name,document_number,document_datesuccessions
accounts.personal[] number,type,status,open_date,close_date,open_treasury,service_treasury,account_organization,public_legal_entityfacialAccounts
accounts.financial_authority[] number,type_name,authority_code,authority_namefoAccounts
accounts.treasury[] number,type_name,open_date,close_date,open_treasury,public_legal_entity,service_treasury_refksaccounts
permissions.participant[] code,name,start_date,end_dateparticipantPermissions
permissions.non_participant[] base fields + registry_number,budget,public_legal_entity,budget_chapternonParticipantPermissions
permissions.procurement[] code,name,start_date,end_dateprocurementPermissions
permissions.accepted[] base + budget,public_legal_entity,budget_chapter,giver,user_area,registry_numberacceptAuths
permissions.transferred[] registry_number,start_date,end_date,budget_code,budget_chapter_code,municipalitiestransfauth
permissions.budget_participant[] budget,budget_level,public_legal_entity,budget_chapter_code,budget_chapter_nameubptransfauthbp
permissions.budget_institution[] Пока без named element schema: непустой ubptransfauthbu временно входит в unclassified_blocks
contracts[] number,sign_date,organization_code,organization_name,budget_codecontracts
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_organization record type;
  • required common record fields и organization name/inn/ogrn/okpo;
  • именованные BudgetRegistryRecordListPayload и BudgetRegistryRecordDetailPayload;
  • named schemas для всех nested collections и CodeName;
  • discriminator по source + record_type либо документированный oneOf;
  • named OrganizationSourceRecordPagination вместо unknown meta;
  • 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