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
1274 lines
70 KiB
Markdown
1274 lines
70 KiB
Markdown
---
|
||
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 |
|