Files
mostovik-backend/docs/source-integration/source-records-backend-improvements.md
Aleksandr Meshchryakov 18971d33ec
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s
feat: complete published registry contracts and gated SRO ingestion
2026-09-14 17:01:02 +02:00

528 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 только после
зелёных интеграционных проверок.