--- template_id: source-backend-api template_version: 1 source_id: 'budget-process-registry' source_name: 'Реестр участников бюджетного процесса' document_status: 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н](https://www.consultant.ru/document/cons_doc_LAW_175321/). Проверенная структура 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` | Минимальный объект: ```json { "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//.../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-поля остаются датами. Каноническая бизнес-ошибка: ```json { "success": false, "errors": [ { "code": "validation_error", "message": "Некорректный параметр source.", "field": "source" } ], "meta": { "request_id": "7ba5ed18-29e2-4d85-b693-928ffc41f5ee" }, "data": null } ``` Auth middleware может вернуть только отдельно описанный ответ: ```json { "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: ```text GET /api/v1/sources/ Authorization: Bearer ``` Response `200 application/json` содержит среди `data`: ```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": [] } ] } ``` `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: ```text GET /api/v1/sources/budget-process-registry/ Authorization: Bearer ``` Response `200 application/json`: ```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: ```text GET /api/v1/parsers/dashboard/ Authorization: Bearer ``` Response-фрагмент `200 application/json` внутри полного named `ParserDashboardResponse`: ```json { "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: ```text 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 ``` 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`: ```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: ```text GET /api/v2/organization-source-records/2e5db996-e17d-4f5e-a898-bb29566927e4/ Authorization: Bearer ``` Response `200 application/json` — plain record без envelope, общие поля идентичны list; `payload` заменяется `BudgetRegistryRecordDetailPayload`: ```json { "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`. 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: ```text POST /api/v1/sources/budget-process-registry/refresh/ Authorization: Bearer Content-Type: application/json {"params":{}} ``` Response `202 application/json`: ```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: ```text POST /api/v1/parsers/run/budget_ubpandnubp/ Authorization: Bearer ``` Response `201 application/json`: ```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: ```text GET /api/v1/jobs/cce43750-3c50-48b7-aade-22cf4eb6cf87/ Authorization: Bearer ``` Response `200 application/json`: ```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 вызван ошибочно: ```text POST /api/v1/parsers/upload/budget_ubpandnubp/ Authorization: Bearer ``` Response `405 application/json`: ```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: ```text GET /api/v1/system/logs/?source=budget_ubpandnubp&ordering=-updated_at&page=1&page_size=100 Authorization: Bearer ``` Response `200 application/json`: ```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: ```text GET /api/v1/system/logs/8201/ Authorization: Bearer ``` Response `200 application/json`: ```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: ```text GET /api/v1/system/logs/export/?source=budget_ubpandnubp&ordering=-updated_at Authorization: Bearer ``` Response `200`: ```text 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`: ```json { "sources": ["budget_process_registry"], "format": "xlsx" } ``` Response `201 application/json`: ```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: ```text POST /api/v2/organization-source-records/export-download/ Content-Type: application/x-www-form-urlencoded ticket=opaque-single-use-token ``` Response `200`: ```text 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 может принять: ```json { "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` | `Обновлено` | История показывает точный исход. Допустимые переходы: ```text 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 после публикации выполняет: ```bash 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 |