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
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:
144
docs/frontend-integration-2026-09-13.md
Normal file
144
docs/frontend-integration-2026-09-13.md
Normal 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:76–135` посылает
|
||||
неподдержанные sort по support_form, support_kind, support_until, provider,
|
||||
region, sme_category, has_violation.
|
||||
`model/source-detail/useBudgetProcessRegistrySourceTable.ts:93–129` — по
|
||||
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`.
|
||||
|
||||
Бюджетная таблица на строках 252–259 отправляет `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` (строки 120–122): согласовать подпись/значение колонки с полем
|
||||
фильтра. Это разные даты.
|
||||
|
||||
**MV-3. Завершать polling по статусу и считать всю группу задач.**
|
||||
`model/sources/useSourceCardRefreshTracking.ts:128–153` по-прежнему считает
|
||||
progress=100 завершением и успехом даже для status=running или status=error
|
||||
с пустым error. Оба результата воспроизведены. Статус jobs API —
|
||||
`running`, `success`, `error`; 100% не доказывает завершение. Terminal и success
|
||||
определять по статусу; ошибка должна оставаться ошибкой.
|
||||
|
||||
Строки 180–203 делят сумму только на уже полученные числовые ответы. Если из
|
||||
двух task_ids первая задача ответила 100, а вторая временно недоступна,
|
||||
расчёт показывает 100; после ответа второй с 0 даёт 50. Свежий
|
||||
`ui/SourceDetailPage/components/useSourceDetailRefresh.ts:107–128` уже удерживает
|
||||
показанный максимум: пользователь теперь может видеть преждевременные 100,
|
||||
а не снижение до 50. Исправить знаменатель на фиксированный полный task_ids,
|
||||
сохранять последние значения при временной ошибке polling. После перезагрузки,
|
||||
если полный набор неизвестен, использовать агрегированный backend progress:
|
||||
текущий приоритет среднего только activeTasks на строках 73–102 исключает
|
||||
завершённые части группы. Строки 87–88 и 130–133 показывают 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:86–93`.
|
||||
Общий 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-1–MV-3: оба реальных API списка и детали открываются без
|
||||
TypeError; отсутствующие поля не подменяются утверждением «нет»; доступны только
|
||||
работающие filters/sorts; running100 продолжает polling, error100 остаётся
|
||||
ошибкой; две задачи с задержанным ответом не показывают преждевременные 100%;
|
||||
после reload сохраняется прогресс всей группы. Проверить 409/refetch, экспорт
|
||||
карточки, серверный период истории и настоящий ZIP общей выгрузки. Использовать
|
||||
fixtures реального backend и стенд без frontend mock-режима.
|
||||
93
docs/registry-sources-ru.md
Normal file
93
docs/registry-sources-ru.md
Normal 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 с рабочей машины отличалась от серверной.
|
||||
52
docs/september-checkup-shared-api.md
Normal file
52
docs/september-checkup-shared-api.md
Normal 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 до
|
||||
завершения предыдущего.
|
||||
Reference in New Issue
Block a user