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
528 lines
38 KiB
Markdown
528 lines
38 KiB
Markdown
# Source records — Backend Improvements
|
||
|
||
Назначение: единый handoff для backend по трём источникам, которые пока работают через
|
||
frontend-моки:
|
||
|
||
- «Реестр участников бюджетного процесса»;
|
||
- «Реестр субъектов МСП — получателей поддержки»;
|
||
- «Проверка членства в СРО».
|
||
|
||
Документ составлен после генерации клиента командой `bun run apigen` из актуального
|
||
`openapi.json`. Он фиксирует не только наличие URL, но и весь контракт, необходимый, чтобы
|
||
frontend мог выключить list/detail-моки, использовать сгенерированный Orval-клиент и сохранить
|
||
таблицы, фильтры, карточки источников, обновление и выгрузку.
|
||
|
||
Важно:
|
||
|
||
- универсальные URL `organization-source-records` уже существуют, но сами по себе не являются
|
||
достаточной реализацией источника;
|
||
- все перечисленные изменения должны быть отражены одновременно в runtime backend, `openapi.json`
|
||
и contract tests backend;
|
||
- после изменения спецификации frontend запускает `bun run apigen`; вручную править сгенерированные
|
||
файлы нельзя;
|
||
- до поставки соответствующего runtime-контракта моки не удаляются.
|
||
|
||
### Граница этого handoff
|
||
|
||
Это самостоятельное техническое задание на доработку текущего backend и OpenAPI. Детальные
|
||
исходные правила нормализации, upstream mapping и расширенные fixture-примеры остаются в
|
||
source-first спецификациях; они обязательны и не могут противоречить этому документу.
|
||
|
||
| Источник | Полная source-first спецификация |
|
||
| ----------------- | -------------------------------------------------------------------------------- |
|
||
| Бюджетный процесс | `docs/source-integration/sources/budget-process-registry/backend-api.md` |
|
||
| Поддержка МСП | `docs/source-integration/sources/sme-support-recipients-registry/backend-api.md` |
|
||
| Членство в СРО | `docs/source-integration/sources/sro-membership-check/backend-api.md` |
|
||
|
||
Если backend-реализация выбирает между кратким описанием ниже и обязательным полем/инвариантом
|
||
в source-first спецификации, применяется source-first спецификация. Изменение контракта требует
|
||
синхронно изменить оба документа, runtime API и `openapi.json`.
|
||
|
||
## Идентификаторы источников
|
||
|
||
| Источник | Source card slug | `source_group` | `source` / parser key | `record_type` | Export group |
|
||
| ------------------------------------- | --------------------------------- | ------------------------- | ---------------------------- | ------------------------------ | ------------------------- |
|
||
| Реестр участников бюджетного процесса | `budget-process-registry` | `budget_process_registry` | `budget_ubpandnubp` | `budget_registry_organization` | `budget_process_registry` |
|
||
| Реестр получателей поддержки МСП | `sme-support-recipients-registry` | `government_support` | `fns_sme_support_recipients` | `sme_support_measure` | `government_support` |
|
||
| Проверка членства в СРО | `sro-membership-check` | `sro_membership` | `sro_membership_check` | `sro_membership` | `sro_membership` |
|
||
|
||
Идентификаторы — канонические. Frontend может понимать aliases маршрута, но никогда не передаёт
|
||
их в API вместо значений из таблицы.
|
||
|
||
## Доработки существующих endpoint'ов
|
||
|
||
### `organization-source-records`
|
||
|
||
Затрагиваемые endpoint'ы:
|
||
|
||
- `GET /api/v2/organization-source-records/`
|
||
- `GET /api/v2/organization-source-records/{uid}/`
|
||
- `POST /api/v2/organization-source-records/export-ticket/`
|
||
- `POST /api/v2/organization-source-records/export-download/`
|
||
|
||
Проблема:
|
||
|
||
- актуальная OpenAPI описывает общий list/read, но `source_group` содержит только
|
||
`budget_process_registry` и `government_support`; `sro_membership` отсутствует;
|
||
- list endpoint описывает только базовые query-параметры. Параметры фильтров, которыми обладают
|
||
экраны и моки трёх источников, не попадают в generated TypeScript client;
|
||
- `payload` остаётся смешанным универсальным объектом: бюджетные вложенные блоки сужены до
|
||
`Record<string, string>`, а для СРО нет полей вообще. Это не позволяет заменить временные DTO
|
||
сгенерированными безопасными типами;
|
||
- `meta` list response свободный object, хотя frontend рассчитывает на
|
||
`meta.pagination.total_count` для серверной пагинации;
|
||
- export request не принимает `sro_membership`.
|
||
|
||
Нужно:
|
||
|
||
- сохранить общий URL, но описать ответ как discriminated union по паре
|
||
`source_group` + `record_type`;
|
||
- вынести в named schemas общую организацию, общую строку, pagination и отдельные list/detail
|
||
payload каждого из трёх источников;
|
||
- сделать обязательными для published record: `uid`, `source_group`, `source`, `record_type`,
|
||
`external_id`, `title`, `status`, `created_at`, `updated_at`, `organization.uid`,
|
||
`organization.name`, `organization.inn`, `organization.ogrn`, `organization.okpo`;
|
||
- сохранять nullable именно как JSON `null`, а не как `""`, `"—"` или отсутствующее поле;
|
||
- добавить `sro_membership` во все применимые enum: list `source_group`, export `sources`,
|
||
source extension, карточка организации и другие фильтры, использующие группу источника;
|
||
- описать `meta.pagination` отдельной схемой с required полями `page`, `page_size`,
|
||
`total_count`, `total_pages`, `has_next`, `has_previous`;
|
||
- фильтровать и сортировать до pagination; стабильно разрешать ties по `uid`, null — последними;
|
||
- поддерживать `GET /{uid}/` для каждого `uid`, возвращённого соответствующим list endpoint;
|
||
- возвращать `400` с машиночитаемым кодом для неизвестного filter/order, `404` для отсутствующей
|
||
published записи, а не заменять их пустым успешным ответом.
|
||
|
||
Минимальный общий envelope списка:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [],
|
||
"errors": null,
|
||
"meta": {
|
||
"pagination": {
|
||
"page": 1,
|
||
"page_size": 50,
|
||
"total_count": 0,
|
||
"total_pages": 0,
|
||
"has_next": false,
|
||
"has_previous": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Форма detail response должна быть единой для всех групп. Рекомендуемый и совместимый с текущим
|
||
frontend вариант — plain source record без envelope:
|
||
|
||
```json
|
||
{
|
||
"uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
|
||
"source_group": "<canonical-source-group>",
|
||
"source": "<canonical-source>",
|
||
"record_type": "<canonical-record-type>",
|
||
"payload": {},
|
||
"organization": {}
|
||
}
|
||
```
|
||
|
||
Запрещено возвращать для одной группы plain record, а для другой `{ "data": record }`:
|
||
generated функция и Zod schema должны иметь одну форму ответа. В list payload допускаются только
|
||
поля, необходимые таблице; detail дополняет, но не изменяет значения общих/list полей.
|
||
|
||
#### Реестр участников бюджетного процесса
|
||
|
||
Текущий запрос таблицы:
|
||
|
||
```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
|
||
```
|
||
|
||
Нужно добавить в OpenAPI и runtime list endpoint следующие параметры.
|
||
|
||
| Параметр | Тип | Правило |
|
||
| ---------------------------- | -------------------- | -------------------------------------------------------- |
|
||
| `source_group` | enum | `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 | Наименование, ИНН, КПП, ОГРН, ОКПО, код/реестровый номер |
|
||
| `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 | `payload.summary.has_procurement_permission` |
|
||
| `ordering` | enum/comma-separated | Allowlist ниже; каждый ключ допускает префикс `-` |
|
||
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
|
||
|
||
Allowlist `ordering`:
|
||
|
||
```text
|
||
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
|
||
```
|
||
|
||
`BudgetRegistryRecordListPayload` должен включать:
|
||
|
||
```ts
|
||
interface CodeName {
|
||
code: string
|
||
name: string
|
||
}
|
||
|
||
interface BudgetRegistryRecordListPayload {
|
||
registry: {
|
||
code: string
|
||
registration_number: string | null
|
||
record_number: string | null
|
||
}
|
||
classification: {
|
||
organization_type: CodeName | null
|
||
establishment_kind: CodeName | null
|
||
}
|
||
budget: {
|
||
level: CodeName | null
|
||
code: string | null
|
||
name: string | null
|
||
}
|
||
address: { region: CodeName | null }
|
||
is_separate_division: boolean | null
|
||
summary: {
|
||
activities_count: number
|
||
authorities_count: number
|
||
permissions_count: number
|
||
accounts_count: number
|
||
successions_count: number
|
||
has_procurement_permission: boolean
|
||
}
|
||
}
|
||
```
|
||
|
||
`GET /{uid}/` обязан возвращать `BudgetRegistryRecordDetailPayload`, расширяющий list payload.
|
||
Это не произвольный `object`: все блоки и коллекции ниже должны быть named OpenAPI schemas.
|
||
|
||
| Блок detail payload | Обязательная структура |
|
||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `registry` | `code`, `registration_number`, `record_number`, `guid`, `parent_record_number`, `status_code`, `status_name`, `registration_date`, `inclusion_date`, `exclusion_date`, `start_date`, `end_date`, `updated_at` |
|
||
| `legal` | `firm_name`, `name_in_documents`, `legal_form`, `ownership_form`, `legal_person_kind` |
|
||
| `classification` | list-поля, `government_body`, `flags` с признаками и `contour_type_code` |
|
||
| `budget` | list-поля, `public_legal_entity`, `budget_chapter`, `authorized_organization`, `treasury_body` |
|
||
| `address` | `full`, `postal_code`, `country`, `region`, `district`, `city`, `locality`, `street`, `house`, `building`, `apartment`, `oktmo`, `okato`, `kladr_code` |
|
||
| `hierarchy` | `founder_kind`, `founder_place`, `creator_kind`, `creator_place`, `parent_organization`, `division_parent` |
|
||
| `reorganization` | `code`, `name`, `document`, `document_number`, `document_date`, `start_date`, `end_date` |
|
||
| `upstream_audit` | `source_version`, `load_date`, first/last registration GUID, last registration number, control/bid/update number и reason |
|
||
| `heads[]` | `full_name`, `position`, `is_primary`, `document_name`, `document_number`, `document_date` |
|
||
| `contacts[]` | `phone`, `email`, `website` |
|
||
| `activities[]` | `code`, `name`, `kind` |
|
||
| `authorities[]` | `code`, `name`, `permissions[]: { code, name }` |
|
||
| `permissions` | named arrays `participant`, `non_participant`, `procurement`, `accepted`, `transferred`, `budget_participant`, `budget_institution` |
|
||
| `accounts` | named arrays `personal`, `financial_authority`, `treasury` |
|
||
| `successions[]`, `contracts[]`, `attachments[]` | отдельные named item schemas; fields не сериализуются в одну display-строку |
|
||
| `unclassified_blocks` | `Record<string, JsonObject[]>` только для непустого неизвестного upstream block, с raw lineage и schema-drift metric |
|
||
|
||
Коллекции всегда массивы, даже если пусты. Неизвестный непустой upstream block не выбрасывается:
|
||
он сохраняется в `unclassified_blocks`, маркируется schema-drift metric и получает named schema
|
||
после появления репрезентативного sample. Frontend не должен реконструировать коллекции из текста
|
||
или повторно обращаться к Budget.gov.
|
||
|
||
Инварианты:
|
||
|
||
- без `status` list возвращает все published статусы;
|
||
- `records_count` карточки источника равен `total_count` нефильтрованного list на одном snapshot;
|
||
- `organizations_count` — distinct `organization.uid`, а покрытие ОПК считается отдельно;
|
||
- следующий failed import не заменяет последний успешный published snapshot;
|
||
- поля-идентификаторы остаются строками, включая ведущие нули.
|
||
|
||
#### Реестр субъектов МСП — получателей поддержки
|
||
|
||
Текущий запрос таблицы:
|
||
|
||
```text
|
||
GET /api/v2/organization-source-records/?source_group=government_support&source=fns_sme_support_recipients&record_type=sme_support_measure&page=1&page_size=50&ordering=-record_date,-external_id
|
||
```
|
||
|
||
Нужно добавить в OpenAPI и runtime list endpoint параметры:
|
||
|
||
| Параметр | Тип | Правило |
|
||
| ------------------------------------------ | -------------------- | ----------------------------------------------------------------- |
|
||
| `source_group` | enum | `government_support` |
|
||
| `source` | enum/string | `fns_sme_support_recipients` |
|
||
| `record_type` | enum/string | `sme_support_measure` |
|
||
| `organization` | UUID | Точный `organization.uid` |
|
||
| `search` | string, max 200 | Организация, ИНН/ОГРН/ОКПО, номер поддержки, поставщик, форма/вид |
|
||
| `sme_category` | enum/string | Категория МСП |
|
||
| `region_code` | string | Код региона обогащённой организации |
|
||
| `support_form_code` | string | Точный upstream code формы |
|
||
| `support_kind_code` | string | Точный upstream code вида |
|
||
| `provider_inn` | string | Ровно 10 цифр |
|
||
| `has_violation` | boolean | Наличие нарушения |
|
||
| `support_unit` | enum | `RUB`, `square_meter`, `hour`, `percent`, `unit` |
|
||
| `amount_from`, `amount_to` | decimal string | Только RUB, включительно |
|
||
| `registry_entry_from`, `registry_entry_to` | date | Дата внесения в XML |
|
||
| `support_until_from`, `support_until_to` | date | Срок поддержки |
|
||
| `termination_from`, `termination_to` | date | Дата прекращения |
|
||
| `ordering` | enum/comma-separated | Allowlist ниже, `-` допускается |
|
||
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
|
||
|
||
Allowlist `ordering`:
|
||
|
||
```text
|
||
record_date, external_id, extension__organization__name,
|
||
extension__organization__inn, extension__organization__ogrn,
|
||
extension__organization__okpo, payload__support_form__name,
|
||
payload__support_kind__name, payload__support_until,
|
||
payload__provider__name, payload__sme_category__name,
|
||
payload__has_violation, amount
|
||
```
|
||
|
||
Нужны named schemas `SmeSupportRecordListPayload` и `SmeSupportRecordDetailPayload`.
|
||
|
||
List payload должен содержать `support_registry_number`, `recipient_type`, `sme_category`,
|
||
`region`, `support_form`, `support_kind`, даты решения/срока/прекращения/внесения,
|
||
`support_sizes[]`, `provider`, `has_violation`, количество нарушений и документов, дату snapshot.
|
||
Ниже — обязательные правила типов, без которых типизированный mapper заменить моки не сможет.
|
||
|
||
| Поле | Тип и инвариант |
|
||
| ----------------- | --------------------------------------------------------------------------------------------------------------- | -------------- | ------ | --------- | -------------------------------- |
|
||
| Dictionary | `{ code: string, name: string }`; не локализованная одиночная строка |
|
||
| `support_sizes[]` | непустой массив `{ unit_code: string, unit: 'RUB' | 'square_meter' | 'hour' | 'percent' | 'unit', value: decimal-string }` |
|
||
| `amount` | сумма только RUB-size либо `null`; часы, проценты и единицы не конвертируются в деньги |
|
||
| `region` | dictionary с nullable provenance; источник значения сохраняется |
|
||
| `provider` | `name`, `inn`, nullable `organization_uid`, `okpo`, `region`, `oktmo`; enrichment не заменяет upstream name/ИНН |
|
||
| Даты | ISO `YYYY-MM-DD`; `termination_date` и `source_updated_date` nullable |
|
||
| Счётчики | `violation_count` и `regulatory_documents_count` — integer, ноль не заменяется `null` |
|
||
|
||
Нельзя передавать отформатированную валюту или склеенные documents/violations вместо typed arrays.
|
||
|
||
Detail payload дополнительно обязан включать `source_document_id`, полный массив
|
||
`regulatory_documents`, полный массив `violations` и provenance snapshot. В объекте нарушения
|
||
нужны `type`, `recognized_date`, `remedy_deadline`, `remedied_date`; пустые значения — `null`.
|
||
`regulatory_documents` и `violations` всегда массивы. Enrichment поставщика nullable и не
|
||
подменяет исходные `provider.name` и `provider.inn`.
|
||
|
||
Кроме перечисленных list-параметров backend должен принять временные совместимые aliases
|
||
`support_form` и `support_kind`, если они ещё используются активным frontend callsite. Канонические
|
||
параметры, отражённые в OpenAPI, — `support_form_code` и `support_kind_code`; aliases не должны
|
||
появляться в новых frontend-вызовах и должны быть удалены только в согласованном breaking change.
|
||
|
||
#### Проверка членства в СРО
|
||
|
||
Проблема:
|
||
|
||
- в текущем `openapi.json` нет ни одного идентификатора источника СРО: `sro_membership`,
|
||
`sro_membership_check`, `sro-membership-check`;
|
||
- `source_group` list/export не принимает `sro_membership`, а payload list/detail не содержит
|
||
status членства, СРО, регион или дату допуска;
|
||
- следовательно, общий URL нельзя безопасно вызвать с generated DTO для данного экрана и
|
||
переключение off моков невозможно.
|
||
|
||
Целевой запрос:
|
||
|
||
```text
|
||
GET /api/v2/organization-source-records/?source_group=sro_membership&source=sro_membership_check&record_type=sro_membership&page=1&page_size=50&ordering=organization__name,payload__sro_name,payload__admission_date
|
||
```
|
||
|
||
Нужно добавить параметры:
|
||
|
||
| Параметр | Тип | Правило |
|
||
| ------------------------------------------ | -------------------- | --------------------------------------------- |
|
||
| `source_group` | enum | `sro_membership` |
|
||
| `source` | enum/string | `sro_membership_check` |
|
||
| `record_type` | enum/string | `sro_membership` |
|
||
| `organization` | UUID | Точный `organization.uid` |
|
||
| `search` | string, max 200 | name/full name, ИНН, ОГРН, ОКПО, название СРО |
|
||
| `membership_status` | enum | `active`, `excluded` |
|
||
| `region` | string | Точный нормализованный регион |
|
||
| `sro_id` | string | Точный resolved ID СРО |
|
||
| `admission_date_from`, `admission_date_to` | date | Включительно |
|
||
| `ordering` | enum/comma-separated | Allowlist ниже, `-` допускается |
|
||
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
|
||
|
||
Allowlist `ordering`:
|
||
|
||
```text
|
||
organization__name, organization__full_name, organization__inn,
|
||
organization__ogrn, organization__okpo, payload__membership_status,
|
||
payload__region, payload__sro_name, payload__admission_date, updated_at
|
||
```
|
||
|
||
Нужны named schemas:
|
||
|
||
```ts
|
||
interface SroMembershipListPayload {
|
||
membership_status: 'active' | 'excluded'
|
||
membership_status_raw: string
|
||
region: string
|
||
sro_id: string
|
||
sro_name: string
|
||
sro_url: string
|
||
admission_date: string | null
|
||
source_registry_date: string | null
|
||
}
|
||
|
||
interface SroMembershipDetailPayload extends SroMembershipListPayload {
|
||
source_record_url: string
|
||
retrieved_at: string
|
||
admission_date_missing_reason: 'not_found' | 'sro_page_unresolved' | 'parse_error' | null
|
||
lineage: {
|
||
lookup_key: 'inn' | 'ogrn'
|
||
lookup_value: string
|
||
sro_resolution: 'row_link' | 'same_site_id' | 'same_site_sitemap'
|
||
}
|
||
}
|
||
```
|
||
|
||
Одна строка — одно членство одной организации в одной СРО. Организация с несколькими СРО
|
||
возвращается несколькими стабильными `uid`. `sro_url` и `source_record_url` должны быть HTTPS
|
||
URL только в allowlist `*.reestr-sro.ru`; backend не должен передавать произвольный upstream URL.
|
||
|
||
Required/nullable инварианты detail:
|
||
|
||
- `organization.name`, `organization.full_name`, ИНН, ОГРН, ОКПО, `membership_status`,
|
||
`membership_status_raw`, `region`, `sro_id`, `sro_name`, `sro_url` обязательны;
|
||
- `admission_date`, `source_registry_date` и `admission_date_missing_reason` nullable;
|
||
- если `admission_date` задана, `admission_date_missing_reason` строго `null`;
|
||
- если `admission_date = null`, причина отсутствия строго non-null;
|
||
- `record_date` равна дате допуска при её наличии; `status` согласован с нормализованным
|
||
`membership_status`;
|
||
- unsafe/missing URL, конфликт ИНН/ОГРН, неизвестный membership status и отсутствие ОКПО не
|
||
публикуются как valid record: они попадают в quarantine с наблюдаемым кодом ошибки.
|
||
|
||
### `sources`
|
||
|
||
Затрагиваемые endpoint'ы:
|
||
|
||
- `GET /api/v1/sources/`
|
||
- `GET /api/v1/sources/{slug}/`
|
||
- `POST /api/v1/sources/{slug}/refresh/`
|
||
|
||
Нужно зарегистрировать все три source cards и отдавать их из списка и detail endpoint с
|
||
согласованными slug, counts, `source_items`, `latest_load`, `latest_success_load` и
|
||
`active_tasks`. Source item обязан содержать точный parser/source key из таблицы идентификаторов.
|
||
|
||
Для каждой карточки:
|
||
|
||
- `records_count` — количество published source records текущего snapshot;
|
||
- `organizations_count` — distinct `organization.uid` текущего snapshot;
|
||
- `last_updated_at` и latest load — дата именно последней успешной публикации, а не запуска;
|
||
- detail и list не расходятся по статусу, counts и активной task;
|
||
- `404` на неизвестный slug, а не пустая успешная карточка.
|
||
|
||
Refresh принимает `{}` или `{ "params": {} }`, ставит задачу в очередь и отвечает `202`:
|
||
|
||
```json
|
||
{
|
||
"status": "queued",
|
||
"task_ids": ["cce43750-3c50-48b7-aade-22cf4eb6cf87"]
|
||
}
|
||
```
|
||
|
||
Повторный активный запуск той же карточки возвращает typed `409 refresh_already_running` и не
|
||
создаёт competing snapshot. Для СРО до получения разрешения владельца upstream допустим
|
||
`409 upstream_access_not_approved`; это отдельная бизнес-ошибка, не пустой успешный refresh.
|
||
|
||
### `parsers`, `jobs` и `system logs`
|
||
|
||
Затрагиваемые endpoint'ы:
|
||
|
||
- `GET /api/v1/parsers/dashboard/`
|
||
- `POST /api/v1/parsers/run/{source_key}/`
|
||
- `GET /api/v1/jobs/{task_id}/`
|
||
- `GET /api/v1/system/logs/`, `GET /api/v1/system/logs/{id}/`,
|
||
`GET /api/v1/system/logs/export/`
|
||
|
||
Нужно:
|
||
|
||
- добавить три parser source key в dashboard, parser run и history enum/filter:
|
||
`budget_ubpandnubp`, `fns_sme_support_recipients`, `sro_membership_check`;
|
||
- для parser run не требовать внешние URL, pagination или upload-параметры: настройки источника
|
||
принадлежат backend;
|
||
- документировать response parser run (`task_id`, `status`, `source`) и 409 duplicate run;
|
||
- в job detail возвращать typed `queued`, `running`, `retrying`, `success`, `failed`,
|
||
`cancelled`, `skipped`, целочисленный `progress`, сообщение и source-specific result;
|
||
- отображать counts, registry/snapshot date и errors одного published snapshot в dashboard и
|
||
history;
|
||
- добавить source label и canonical parser source в list/detail/export логов.
|
||
|
||
### `export-ticket` и `export-download`
|
||
|
||
Текущий OpenAPI уже содержит ticket/download URL, но export enum не принимает
|
||
`sro_membership`.
|
||
|
||
Нужно:
|
||
|
||
- добавить все значения из колонки Export group в `OrganizationSourceRecordExportRequest.sources`;
|
||
- сохранить асинхронный ticket flow: `POST export-ticket` → `201` с `ticket`, `expires_in`,
|
||
`file_name`; затем form POST на `export-download`;
|
||
- сделать ticket одноразовым и короткоживущим; после истечения/повторного применения возвращать
|
||
typed `410 ticket_expired`;
|
||
- выгружать именно тот published snapshot, который соответствует source records, не raw/imported
|
||
данные;
|
||
- поддержать JSON, CSV и XLSX в соответствии с declared capability источника; значения ИНН,
|
||
ОГРН, ОКПО, IDs и номера реестра оставлять строками.
|
||
|
||
## Регрессия после `bun run apigen`: тип ordering
|
||
|
||
Проблема:
|
||
|
||
- генерация актуальной схемы успешно завершается, но `bun run type-check` завершается ошибкой;
|
||
- из generated `endpoints.schemas.ts` исчез экспорт
|
||
`V2OrganizationSourceRecordsListOrdering`;
|
||
- его импортируют `useMassMediaSourceTable.ts` и `useRopkSanctionsSourceTable.ts`.
|
||
|
||
Причина:
|
||
|
||
- параметр `ordering` у `GET /api/v2/organization-source-records/` стал просто `string`;
|
||
- Orval генерирует отдельный alias `V2OrganizationSourceRecordsListOrdering` только для OpenAPI
|
||
enum. Поэтому эта регрессия относится к контракту, а не к настройке Orval.
|
||
|
||
Нужно:
|
||
|
||
- вернуть `ordering` как named enum/schema в OpenAPI list operation;
|
||
- включить в него все уже поддерживаемые ключи СМИ и санкций, а также ключи трёх источников из
|
||
этого документа;
|
||
- поддержать на runtime все значения enum, включая обратную сортировку с `-` и несколько полей
|
||
через запятую, либо явно заменить frontend-контракт отдельным согласованным string type. Первый
|
||
вариант предпочтителен: он сохраняет совместимость generated API;
|
||
- после изменения выполнить `bun run apigen` и `bun run type-check` в backend/frontend CI.
|
||
|
||
Минимальный набор существующих ключей, который нельзя потерять:
|
||
|
||
```text
|
||
record_date, external_id, extension__organization__name,
|
||
payload__news_source, payload__sentiment,
|
||
payload__united_states, payload__united_states_sectoral,
|
||
payload__uk_hm_treasury, payload__uk_uksl,
|
||
payload__european_union, payload__switzerland, payload__ukraine
|
||
```
|
||
|
||
## Приёмка и последовательность замены моков
|
||
|
||
Backend готов к передаче frontend только после выполнения всех пунктов ниже.
|
||
|
||
- [ ] Runtime API и `openapi.json` содержат одинаковые identifiers трёх источников.
|
||
- [ ] List и detail records имеют named/discriminated schemas для budget, SME support и SRO.
|
||
- [ ] Все фильтры и ordering allowlist из этого документа доступны в runtime и видны Orval после
|
||
`bun run apigen`.
|
||
- [ ] `sro_membership` присутствует во всех требуемых enum, включая records и export.
|
||
- [ ] Pagination является typed и возвращает корректный `total_count` после фильтрации.
|
||
- [ ] Source cards, dashboard, list, detail, jobs и logs считают один published snapshot.
|
||
- [ ] Refresh/parser run возвращают typed task response, duplicate run — typed `409`.
|
||
- [ ] Export ticket/download поддерживает три source group и выдаёт published записи.
|
||
- [ ] Есть backend contract fixtures: zero, один и many records; nullable dates; multiple SRO на
|
||
одну организацию; partial и large detail бюджетного реестра; поддержка с несколькими
|
||
размерами/документами/нарушениями.
|
||
- [ ] `bun run apigen` проходит без ручных изменений generated файлов.
|
||
- [ ] `bun run type-check` проходит, включая СМИ и зарубежные санкции.
|
||
|
||
После этого frontend выполняет: отключает `VITE_BUDGET_PROCESS_REGISTRY_MOCK`,
|
||
`VITE_SME_SUPPORT_RECIPIENTS_MOCK`, `VITE_SRO_MEMBERSHIP_MOCK`; заменяет временные DTO на
|
||
generated schemas/adapters; добавляет runtime contract tests и удаляет mock fixtures только после
|
||
зелёных интеграционных проверок.
|