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

1274 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
template_id: source-backend-api
template_version: 1
source_id: '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/<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-поля остаются датами.
Каноническая бизнес-ошибка:
```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 <token>
```
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 <token>
```
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 <token>
```
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 <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`:
```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 <token>
```
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<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:
```text
POST /api/v1/sources/budget-process-registry/refresh/
Authorization: Bearer <admin-token>
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 <admin-token>
```
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 <token>
```
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 <admin-token>
```
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 <admin-token>
```
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 <admin-token>
```
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 <admin-token>
```
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 |