feat(registries): add SME and budget imports and fix source API workflows
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 9m37s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 4m18s
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 1m48s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-13 23:13:47 +02:00
parent c863563851
commit 49cbfd265c
50 changed files with 5021 additions and 452 deletions

View File

@@ -0,0 +1,144 @@
# Мостовик: интеграция frontend с backend, 13.09.2026
Передача Глебу. Проверен свежий frontend `origin/dev`
`c6f2ec787efef5617d6624ff83050589c3941f0d` после успешного SSH fetch через
штатный jump host. Рабочее дерево frontend не переключалось и не изменялось.
Backend-контракт: текущая поставка поверх
`c863563851642727045f58f16336aa96fb28308a`; основной контракт новых источников
описан в [registry-sources-ru.md](registry-sources-ru.md).
Документ не подтверждает публикацию backend/frontend на dev-стенде.
Все frontend-пути ниже начинаются с `src/pages/main/`.
**Уже подключено в свежем frontend dev:** оба новых view, таблицы и настройки
запуска, оба источника в фильтре истории и обе группы общей выгрузки. Это
подтверждено чтением `resolveSourceDetailView.ts`, `useScrapingCollectionCards.ts`,
`ReferenceDataSourcesExportCard.vue`, `updateHistoryLogs.ts` и исполнением resolver.
Старое заключение об отсутствии этих подключений к свежему dev не относится.
| Источник | Slug | Source | Source group | Record type |
| --- | --- | --- | --- | --- |
| Поддержка МСП | `sme-support-recipients-registry` | `fns_sme_support_recipients` | `government_support` | `sme_support_measure` |
| Бюджетный процесс | `budget-process-registry` | `budget_ubpandnubp` | `budget_process_registry` | `budget_registry_organization` |
**MV-1. Привести DTO и детали к фактическому MVP payload.**
Backend `src/apps/parsers/budget_registry.py:55` отдаёт нормализованные
`registry`, `classification`, `budget`, `address` и полный исходный `upstream`
в детали. Он не обещает нормализованные `legal`, `heads`, `contacts`, `activities`,
`authorities`, `successions`, `contracts`, `attachments`, `summary`,
`is_separate_division`. Однако frontend
`ui/SourceRecordDetail/BudgetProcessRegistryRecordDetail.vue:98` обращается к
`payload.legal.firm_name`, а
`model/source-record-detail/sourceRecordDetail.ts:400,414` — к
`payload.legal.legal_form` и `.heads.length`. На payload фактического normalizer
исполнение mapper завершилось TypeError `reading 'legal_form'`.
Backend `src/apps/parsers/sme_support.py:78` отдаёт `recipient_type` и
`sme_category` строковыми кодами, `support_form`/`support_kind` словарями,
`provider` с name/inn, `support_sizes` со значением и единицей. `violations` и
`regulatory_documents` сохраняют оригинальные XML-атрибуты. `provenance` и
нормализованного региона в payload нет. Frontend
`model/source-detail/smeSupportRecipients.ts:61,195` ожидает словарь категории
и теряет существующий код при выводе; detail mapper
`model/source-record-detail/sourceRecordDetail.ts:525` падает на
`payload.provenance.dataset_url` (TypeError воспроизведён).
Исправить перечисленные DTO, мапперы и detail renderer под этот контракт:
не читать несуществующие обязательные блоки; не заменять неизвестные признаки
значением «нет»; выводить подтверждённые код/значение/единицу, а не вымышленные
названия. Для бюджета полный исходный документ доступен в `payload.upstream`
детали/выгрузки. Для МСП читать реальные имена XML-атрибутов документов и
нарушений. Отдельно проверить экспорт полной карточки, использующий тот же mapper.
Список GET `/api/v2/organization-source-records/` возвращает `data` и
`meta.pagination`, detail GET `/{uid}/` — объект записи. Краткий payload
намеренно исключает `source_document_id`, `violations`, `regulatory_documents`,
`upstream`: за ними нужен detail-запрос. Отсутствие массива в списке не означает
отсутствие данных. Полный контракт: `docs/registry-sources-ru.md`.
**MV-2. Оставить только поддержанные сортировки и фильтры.**
Backend уже исправлен для составного `ordering` из существующего allowlist:
начальный запрос МСП `ordering=-record_date,-external_id` теперь проходит.
Каждый компонент отдельно валидируется; неизвестные поля остаются 400.
Дополнительные JSON-sort и специальные фильтры в MVP не добавлялись.
`model/source-detail/useSmeSupportRecipientsSourceTable.ts:76135` посылает
неподдержанные sort по support_form, support_kind, support_until, provider,
region, sme_category, has_violation.
`model/source-detail/useBudgetProcessRegistrySourceTable.ts:93129` — по
organization_type, establishment_kind, budget.level, address.region,
is_separate_division. Убрать sortable/orderingKey с этих заголовков до отдельного
согласования backend-расширения. Не подменять сортировку смыслово другим полем.
Допустимые простые поля для соответствующих колонок: `record_date`, `updated_at`,
`status`, `external_id`, `extension__organization__name` (также alias
`organization__name`), ИНН/ОГРН/ОКПО организации. Полный текущий allowlist находится
в `src/organizations/views.py:104`. Рекомендуемые defaults:
МСП `-record_date,-external_id`, бюджет `extension__organization__name`.
Бюджетная таблица на строках 252259 отправляет `budget_level`,
`establishment_kind`, `has_procurement_permission`, `is_branch`,
`organization_type`, `region_code`: текущий backend эти шесть параметров
игнорирует. Скрыть/отключить соответствующие фильтры. Поддержаны общие
source/source_group/record_type, status, organization, search и период record_date.
Колонка бюджета «Обновлено» показывает `updated_at`, но её dateFilterKey —
`record_date` (строки 120122): согласовать подпись/значение колонки с полем
фильтра. Это разные даты.
**MV-3. Завершать polling по статусу и считать всю группу задач.**
`model/sources/useSourceCardRefreshTracking.ts:128153` по-прежнему считает
progress=100 завершением и успехом даже для status=running или status=error
с пустым error. Оба результата воспроизведены. Статус jobs API —
`running`, `success`, `error`; 100% не доказывает завершение. Terminal и success
определять по статусу; ошибка должна оставаться ошибкой.
Строки 180203 делят сумму только на уже полученные числовые ответы. Если из
двух task_ids первая задача ответила 100, а вторая временно недоступна,
расчёт показывает 100; после ответа второй с 0 даёт 50. Свежий
`ui/SourceDetailPage/components/useSourceDetailRefresh.ts:107128` уже удерживает
показанный максимум: пользователь теперь может видеть преждевременные 100,
а не снижение до 50. Исправить знаменатель на фиксированный полный task_ids,
сохранять последние значения при временной ошибке polling. После перезагрузки,
если полный набор неизвестен, использовать агрегированный backend progress:
текущий приоритет среднего только activeTasks на строках 73102 исключает
завершённые части группы. Строки 8788 и 130133 показывают success/«Обновлено»
для любого неактивного состояния: отдельно отображать error и idle.
202 с task_ids уже поддерживается (`queued` вместо `accepted` не мешает).
При 409 `source_refresh_running` показать сообщение и перечитать карточку/
статусы; не включать фиктивный polling без IDs. Общий error envelope совместим,
отдельного восстановления карточки на 409 в hook пока нет.
**История и общая выгрузка уже согласованы по исходникам.**
`model/update-history/useUpdateHistoryTable.ts:55,67,148` теперь отправляет
`date_from/date_to` и в список, и в экспорт, включает период в query key и
сбрасывает страницу. `lib/update-history/updateHistoryLogs.ts:46` корректно
передаёт один день как одинаковые границы. Старый локальный date filter удалён.
Это регрессия для приёмки, не новая задача реализации. Проверить >100 строк,
совпадение периода/количества CSV и список, границу суток: backend фильтрует
updated_at в UTC, browser formatter использует локальную зону.
Обе новые группы уже есть в `ReferenceDataSourcesExportCard.vue:8693`.
Общий ticket 201 / native POST download совместим. В
`model/source-detail/exportSourceDetailTable.ts:111` новые таблицы остаются
на локальном CSV fallback. Если кнопка обещает полный реестр, подключить
серверную выгрузку; если только текущую страницу — явно обозначить объём.
Карточки/списки относятся к ОПК, полная выгрузка — ко всем сопоставленным
организациям справочника; эти счётчики могут отличаться по контракту.
## Проверки и критерии готовности
Составной allowlisted ordering уже исправлен на backend: 29 API-тестов прошли,
Ruff и `git diff --check` прошли. Несуществующие payload-sort поля дают 400.
Node probes свежих frontend-функций воспроизвели ошибки деталей (`legal_form`,
`dataset_url`), потерю кода категории и ошибки определения terminal/progress.
Payload для деталей получен фактическими backend normalizers без сети и БД;
это не полный Vue/browser тест. Frontend build/Vitest/E2E здесь не запускались.
Готовность после MV-1MV-3: оба реальных API списка и детали открываются без
TypeError; отсутствующие поля не подменяются утверждением «нет»; доступны только
работающие filters/sorts; running100 продолжает polling, error100 остаётся
ошибкой; две задачи с задержанным ответом не показывают преждевременные 100%;
после reload сохраняется прогресс всей группы. Проверить 409/refetch, экспорт
карточки, серверный период истории и настоящий ZIP общей выгрузки. Использовать
fixtures реального backend и стенд без frontend mock-режима.

View File

@@ -0,0 +1,93 @@
# Реестры поддержки МСП и участников бюджетного процесса
Источники обновляются администратором через существующую карточку источника или
`POST /api/v1/parsers/run/{source_key}/`. Ответ `202` содержит идентификатор задачи,
доступный сразу через `/api/v1/jobs/{task_id}/`. Одновременный запуск того же
источника возвращает `409`. Автоматические расписания не добавлены.
| Источник | Карточка | Ключ парсера | Группа записей | Тип записи |
| --- | --- | --- | --- | --- |
| Поддержка МСП | `sme-support-recipients-registry` | `fns_sme_support_recipients` | `government_support` | `sme_support_measure` |
| Бюджетный процесс | `budget-process-registry` | `budget_ubpandnubp` | `budget_process_registry` | `budget_registry_organization` |
Записи выдаются универсальными API `/api/v2/organization-source-records/` и
`/api/v2/organization-source-records/{uid}/`. Список содержит краткий payload;
деталь сохраняет полные массивы и исходные бюджетные блоки. Стандартные выгрузки
CSV/XLSX/JSON содержат полный payload, включая данные за прошлые годы.
## Состав организаций и счетчики
Импорт связывает сведения только с существующими организациями канонического
справочника (`directory_imported_at` заполнено). Новые организации из внешних
реестров не создаются. Неоднозначные и конфликтующие идентификаторы не выбираются
автоматически. Для публикации нужны наименование, ИНН, ОГРН и ОКПО организации.
Карточка, список записей и dashboard `source_counts` новых источников используют
существующий фильтр организаций ОПК. Полный импорт и стандартная выгрузка
охватывают все сопоставленные организации справочника, включая организации вне
ОПК. Поэтому `published_records_count` задачи и `published_count` исходного
артефакта могут быть больше счетчика карточки. Raw, пропуски и карантин также
учитываются отдельно.
## Загрузка и публикация
МСП: каталог `https://www.nalog.gov.ru/opendata/7707329152-rsmppp/` определяет
последний ZIP формата `structure-20230615`. Архив скачивается потоково во временный
файл; XML проверяется по сохраненной официальной XSD 4.04. Одна мера поддержки
создает одну стабильную запись. Размеры разных единиц остаются массивом, `amount`
содержит только рубли. Физические лица и получатели вне справочника не
публикуются; полный исходный ZIP сохраняется как артефакт. Лимиты: ZIP 2 GiB,
суммарный распакованный XML 64 GiB, без извлечения путей архива на диск.
ZIP запрашивается диапазонами по 16 MiB; оборванное тело продолжается с последнего
записанного байта, до трех повторов подряд. Продолжение требует совпадения
`Content-Range`, полного размера и сильного `ETag` либо `Last-Modified`; сменившийся
файл не склеивается с прежним. Если сервер сразу не поддерживает Range, повтор
полного ответа начинается с нуля. Размер диапазона и число повторов задаются
внутренними параметрами `download_registry_archive`.
В официальном архиве от 15.08.2026 `КолДок=1` встречается в XML с сотнями
документов. Полноту подтверждают CRC архива, завершенный XML и XSD, а расхождение
этого поля сохраняется в metadata артефакта: `documents_count` — фактическое число,
`declared_documents_count` — сумма заявленных значений,
`document_count_mismatch_files` — число XML с расхождением.
Бюджет: `https://budget.gov.ru/epbs/registry/ubpandnubp/data` обходится по страницам
`blocks=info`. Полная деталь запрашивается и сохраняется только для сопоставленных
организаций. Изменение количества записей или версии во время обхода,
дублирование идентификатора и неполная страница отклоняют новый снимок.
Неизвестные коды статуса остаются `unknown`; документированный код `2` означает
`inactive`. Исходные поля и неизвестные непустые блоки сохраняются в detail
`payload.upstream`, без выдуманного толкования.
Новые данные сначала попадают в staging. Только завершенный и проверенный вход
публикуется одной транзакцией, вместе с финальными статусами журнала и задачи.
Ошибка разбора, публикации, проверки кеша до commit или финализации сохраняет
предыдущие записи. После commit версия кеша меняется повторно, чтобы исключить
обычное заполнение кеша старым снимком во время транзакции. Ошибка этой повторной
инвалидации записывается в журнал, но уже опубликованные данные и успешная задача
не откатываются: общей транзакции базы данных и кеша нет.
Повторная доставка задачи и повторная публикация артефакта не удаляют данные.
Существующая политика хранения артефактов не изменена.
Причина отказа проверяется по `artifact_id` в `ParserSourceArtifact.metadata`:
`error_code` содержит класс ошибки, а `rejection_reason` — ограниченный внутренний
код проверки, например `incomplete_budget_snapshot`. Такие коды также видны в
ошибке задачи и журнале загрузки. Тела HTTP-ответов и XML в ошибки не копируются.
Перед включением на стенде нужны миграции `parsers.0034` и `organizations.0012`,
доступ worker к официальным HTTPS-источникам и место для временного и сохраненного
ZIP. Первый полный импорт и время его выполнения проверяются отдельно: успешное
чтение каталога/XSD не подтверждает успешную загрузку всего реестра.
## Проверенный официальный снимок
13.09.2026 полностью проверен `data-20260815-structure-20230615.zip`:
840 457 773 байта, SHA-256
`00bc1d1ef97e1332f5e59fb04fd230f7ed0e0452ba7e90f6551d357ec891ce7a`.
CRC всех 14 140 XML и полный проход XSD успешны: 3 344 437 документов,
12 580 017 мер поддержки. Все 4 173 762 меры юридических лиц прошли нормализацию
без ошибок; 8 406 255 мер физических лиц посчитаны без нормализации.
Эта проверка не выполняла сопоставление со справочником и публикацию в рабочую
базу. Бюджетный API проверен со стенда: JSON первой страницы и нормализация записи
совместимы; доступность этого API с рабочей машины отличалась от серверной.

View File

@@ -0,0 +1,52 @@
# Общие исправления API: сентябрь 2026
`GET /api/v2/organizations/` и `GET /api/v2/organization-source-records/`
используют одинаковый поиск по каноническим наименованиям (`name`, `full_name`,
`short_name`), ИНН, КПП, ОГРН, ОГРИП и ОКПО. Слова запроса соединяются через AND,
поля для каждого слова — через OR. Совпадение только в `payload`, URL, названии
записи или технических идентификаторах не включает организацию в результат.
Поиск и активные фильтры применяются до пагинации.
`GET /api/v1/system/logs/` и `GET /api/v1/system/logs/export/` принимают
`date_from`/`date_to` в формате `YYYY-MM-DD`. Границы включительны по `updated_at`
в часовом поясе приложения (`TIME_ZONE`, сейчас UTC). Один `date_from` означает
один день; один `date_to` ограничивает только конец периода. Некорректные даты,
перевёрнутый период, неверный `batch_id` или неизвестный `ordering` возвращают 400.
Порядок по умолчанию — `-updated_at, id`. Поддерживаются `updated_at`, `source`,
`status`, `records_count` и обратные направления; сохранены существующие поля
`id`, `batch_id`, `created_at`, `source_label`, `status_label`,
`organizations_count` и сортировка по нескольким полям через запятую.
Вторичный порядок по `id` стабилизирует строки с одинаковым значением.
`source` сортируется по публичному slug карточки. Фильтры и порядок list/export
совпадают, но export не ограничивается страницей. Права администратора сохранены.
История экспортируется как `update-history.csv`: UTF-8 с BOM, разделитель `;`,
CRLF, русские заголовки и подписи источников/статусов. Даты отображаются в часовом
поясе приложения. Первые столбцы: «№», «Дата актуализации», «Источник», «Статус»,
«Количество записей»; далее идут сведения о пакете, организациях и результатах импорта.
В XLSX санкций заголовки `rn`, `ogrn`, `inn`, `okpo` заменены русскими подписями,
включая каждый файл при разбиении выгрузки. Порядок колонок, строковые
идентификаторы, JSON/CSV санкций и ticket/download API сохранены.
После обновления backend необходимо пересобрать подготовленные выгрузки командой
`uv run python src/manage.py build_source_record_exports` в настроенном окружении
сервиса: скачивание отдаёт опубликованные файлы, а не формирует книгу при запросе.
При отмене загрузки вакансий между сохранением записей и контрольной точкой
метаданные завершённой задачи остаются неизменными; следующий запуск повторяет
последнюю организацию от сохранённой позиции в том же batch, включая позицию 0,
если пакет уже содержит записи. Идемпотентное сохранение предотвращает дубликаты.
Прогресс фоновых задач обновляется условным атомарным запросом: позднее меньшее
значение не уменьшает процент, успешное завершение устанавливает 100%, ошибка
сохраняет достигнутый процент и причину. Успех, ошибка и отмена окончательны;
запоздавший callback или запрос отмены не меняет завершённую задачу.
При ручном запуске карточки весь набор `refresh_task_ids` записывается до отправки
первой задачи в очередь. Среднее включает завершённые части запуска. Если старый
разрешённый запуск ещё работает, он остаётся в `active_tasks`; новая завершённая
группа его не скрывает. Новый запуск нового реестра блокируется кодом 409 до
завершения предыдущего.