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
1195 lines
59 KiB
Markdown
1195 lines
59 KiB
Markdown
---
|
||
template_id: source-backend-api
|
||
template_version: 1
|
||
source_id: 'sro-membership-check'
|
||
source_name: 'Проверка членства в СРО'
|
||
document_status: review
|
||
---
|
||
|
||
# Backend API источника «Проверка членства в СРО»
|
||
|
||
> Уточнение 14.09.2026: разделы подготовки выгрузки с `preparing` / HTTP 202
|
||
> заменены [общим runtime-контрактом](../../../source-records-public-runtime-contract.md):
|
||
> готовый ticket HTTP 201 → native form download HTTP 200; устаревшее поколение —
|
||
> HTTP 503 `source_export_not_ready`, истёкший/использованный ticket — HTTP 410 `ticket_expired`.
|
||
|
||
Назначение: backend-first спецификация контрактов, которые требуются frontend для полного
|
||
подключения источника. Документ задаёт DTO, endpoint, фильтры, статусы, ошибки и инварианты
|
||
OpenAPI. Он не предписывает внутреннюю ORM, очередь, HTTP-клиент или конкретную реализацию
|
||
parser `reestr-sro.ru`.
|
||
|
||
## Принципы документа
|
||
|
||
- frontend читает источник только через внутренний backend API;
|
||
- backend не проксирует HTML и не отдаёт cookies внешнего сайта;
|
||
- source-specific read endpoint не создаются: используются универсальные source records;
|
||
- одна запись соответствует членству одной организации в одной СРО;
|
||
- одна организация может иметь zero, one или many записей;
|
||
- `organization.name`, ИНН, ОГРН и ОКПО обязательны во всех опубликованных записях;
|
||
- ОКПО обогащается backend, поскольку `reestr-sro.ru` его не предоставляет;
|
||
- значения идентификаторов всегда strings и не преобразуются в number;
|
||
- nullable-поля возвращаются как `null`, не как `""`, `"—"` или дата загрузки;
|
||
- URL СРО разрешены только внутри `https://reestr-sro.ru` и его поддоменов;
|
||
- runtime, `openapi.json`, generated API, generated Zod и contract tests имеют одну форму;
|
||
- API возвращает данные, enums и raw source values, а пользовательское форматирование делает
|
||
frontend;
|
||
- failed/partial refresh не заменяет последний successful published snapshot;
|
||
- production refresh запрещён до согласования автоматизированного доступа с владельцем сайта.
|
||
|
||
## 1. Назначение и область реализации
|
||
|
||
### Описание источника
|
||
|
||
| Параметр | Значение |
|
||
| ------------------------------ | --------------------------------------------------------------------- |
|
||
| Наименование | `Проверка членства в СРО` |
|
||
| Назначение данных | Действующее и прекращённое членство юридических лиц в СРО |
|
||
| Владелец backend | Команда интеграции источников |
|
||
| Внешняя система / URL | `https://www.reestr-sro.ru/proverka_dopuska/` |
|
||
| Разрешённые внешние hostnames | `reestr-sro.ru`, `www.reestr-sro.ru`, `*.reestr-sro.ru` |
|
||
| Способ получения | Backend-only HTTPS GET, SSR HTML и same-site страницы реестров членов |
|
||
| Периодичность | Daily incremental + monthly full sweep после разрешения upstream |
|
||
| Ожидаемый объём | Число проверяемых организаций × zero/one/many membership rows |
|
||
| Ограничения внешнего API | Bulk API нет; `robots.txt`: `Crawl-delay: 3`, query/search disallow |
|
||
| Политика удаления/актуализации | Immutable raw batches; published snapshot заменяется атомарно |
|
||
| Raw retention | Не менее 90 дней и не менее 10 последних successful batches |
|
||
|
||
Интеграция использует только `*.reestr-sro.ru`. Данные НОПРИЗ, ФНС, сайты отдельных СРО и
|
||
любые другие внешние сайты не являются upstream этого источника. Канонический внутренний
|
||
реестр организаций используется только для связи `organization.uid` и enrichment ОКПО.
|
||
|
||
### Наблюдаемый upstream-контракт
|
||
|
||
Основной SSR lookup:
|
||
|
||
```text
|
||
GET https://www.reestr-sro.ru/proverka_dopuska/
|
||
?q=<exact INN or OGRN>
|
||
&sro_name=
|
||
&sro_id=
|
||
&sro_reg_number=
|
||
®ion_id=
|
||
&search=Найти
|
||
```
|
||
|
||
Результат находится в `table.sro-members`. Отдельного XHR/fetch с результатами компаний нет.
|
||
Наблюдаемая строка содержит:
|
||
|
||
| HTML / подпись | Нормализованное поле | Обязательность |
|
||
| --------------------------------------- | ----------------------------------- | -------------- | -------- |
|
||
| `Краткое:` | `organization.name` | Required |
|
||
| `Полное:` | `organization.full_name` | Required |
|
||
| Ячейка `ИНН, ОГРН`, первая строка | `organization.inn` | Required |
|
||
| Ячейка `ИНН, ОГРН`, вторая строка | `organization.ogrn` | Required |
|
||
| Ячейка `Статус` | `payload.membership_status_raw` | Required |
|
||
| Нормализованный status | `payload.membership_status=active | excluded` | Required |
|
||
| Ячейка `Регион регистрации` | `payload.region` | Required |
|
||
| Текст ячейки `СРО` | `payload.sro_name` | Required |
|
||
| Same-site `a[href]` или resolved SRO ID | `payload.sro_url`, `payload.sro_id` | Required |
|
||
| Текст `Реестр обновлен` | `payload.source_registry_date` | Nullable date |
|
||
|
||
Дата допуска берётся со страницы той же СРО:
|
||
|
||
```text
|
||
https://<regional-host>.reestr-sro.ru/<sro-path>/sro-id-<id>/members/?q=<INN>
|
||
```
|
||
|
||
В `table.sro-members` этой страницы присутствуют `ИНН`, краткое/полное наименование,
|
||
`Дата получения допуска` и `Статус`. Дата нормализуется из `DD.MM.YYYY` в `YYYY-MM-DD`.
|
||
|
||
Наблюдаемые статусы:
|
||
|
||
| Source value | API enum | Generic record status |
|
||
| ----------------- | ---------- | --------------------- |
|
||
| `Является членом` | `active` | `active` |
|
||
| `Исключен` | `excluded` | `inactive` |
|
||
|
||
Неизвестный непустой статус не угадывается: raw row сохраняется в quarantine
|
||
`unknown_membership_status`. Расширение enum требует обновления OpenAPI и frontend mapper.
|
||
|
||
### Граница документа
|
||
|
||
Документ фиксирует входные данные только для обоснования DTO и contract tests. Backend-команда
|
||
самостоятельно выбирает реализацию HTTP, DOM parser, sitemap resolver, concurrency, storage и
|
||
очереди. Наблюдаемые обязательства реализации с точки зрения frontend:
|
||
|
||
1. backend возвращает нормализованные записи через универсальные endpoint;
|
||
2. ни один frontend endpoint не выдаёт upstream HTML;
|
||
3. ОКПО и `organization.uid` уже разрешены;
|
||
4. СРО имеет безопасный same-site URL;
|
||
5. дата допуска — `date | null` с явной причиной отсутствия в metadata;
|
||
6. zero result отличается от ошибки получения/парсинга;
|
||
7. source counts и status согласованы между endpoint.
|
||
|
||
### Покрываемые frontend-сценарии
|
||
|
||
| Сценарий | Требуется | Страница / элемент | Endpoint |
|
||
| ------------------ | --------- | ------------------------------- | --------------------------------------- |
|
||
| Каталог источников | Да | `/sources`, карточка источника | `/api/v1/sources/*` |
|
||
| Главная аналитика | Да | `/main`, таблица и графики | `/api/v1/parsers/dashboard/` |
|
||
| Детальная страница | Да | `/sources/sro-membership-check` | source detail, dashboard, records |
|
||
| Таблицы записей | Да | источник и карточка организации | `/api/v2/organization-source-records/*` |
|
||
| Ручное обновление | Да | `/settings/scraping` | source refresh, parser run, jobs |
|
||
| История обновлений | Да | `/update-history` | `/api/v1/system/logs/*` |
|
||
| Выгрузка данных | Да | настройки → выгрузка источников | source-record export |
|
||
|
||
## 2. Нормативные ссылки и аудит текущих контрактов
|
||
|
||
| Уровень | Репозиторный путь | Что проверить |
|
||
| ------------------------------ | ----------------------------------------------------------- | ----------------------------------------------- |
|
||
| Эталон backend-first документа | `docs/info/backend-endpoints-main-page-from-mocks.md` | request/response, ошибки, инварианты |
|
||
| Общий шаблон | `docs/source-integration/templates/backend-api.template.md` | обязательная структура |
|
||
| Машинная схема | `openapi.json` | paths, named schemas, required, nullable, enums |
|
||
| Генерация клиента | `orval.config.ts` | operationId и результат `bun run apigen` |
|
||
| Generated TypeScript | `src/shared/api/generated-api/` | DTO и функции запросов |
|
||
| Generated Zod | `src/shared/model/generated-zod/` | runtime schemas |
|
||
| Runtime model | `src/pages/main/model/` | adapters, query keys, fallbacks |
|
||
| Контрактные тесты | `src/pages/main/model/__contract__/` | runtime-ответы тестового backend |
|
||
|
||
### Точки сверки по каждому активному endpoint
|
||
|
||
| Endpoint | Текущий reference | Требование источника СРО |
|
||
| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------ |
|
||
| `GET /api/v1/sources/` | `frontend-sources.ts`, `SourceCardListResponse` | Новая карточка и обязательные counts |
|
||
| `GET /api/v1/sources/{slug}/` | `frontend-sources.ts`, `SourceCardDetailResponse` | Detail, source item, active task и latest load |
|
||
| `GET /api/v1/parsers/dashboard/` | `api.ts`, manual runtime parser | Named schema, source counts, schedule и coverage |
|
||
| `GET /api/v2/organization-source-records/` | `organizations.ts`, `sourceRecordsApi.ts` | Новый enum/filter и `SroMembershipListPayload` |
|
||
| `GET /api/v2/organization-source-records/{uid}/` | `api.ts`, `useSourceRecordDetail.ts` | `SroMembershipDetailPayload` |
|
||
| `POST /api/v1/sources/{slug}/refresh/` | `frontend-sources.ts` | Canonical `task_ids` и typed 409 |
|
||
| `POST /api/v1/parsers/run/{source_key}/` | `api.ts`, `sourceCardParserRefresh.ts` | Typed admin run response |
|
||
| `GET /api/v1/jobs/{task_id}/` | `background-jobs.ts` | Membership progress metadata |
|
||
| `GET /api/v1/system/logs/` | `system.ts` | Новый source enum/filter |
|
||
| `GET /api/v1/system/logs/{id}/` | `system.ts` | Counts, registry date и error samples |
|
||
| `GET /api/v1/system/logs/export/` | `system.ts` | Source в CSV и единый `batch_id: integer` |
|
||
| export ticket/download | `exportReferenceDataSourceRecords.ts` | `sourceGroup=sro_membership` |
|
||
|
||
Существующие generated endpoint не считаются подходящими только по факту наличия. После
|
||
изменения OpenAPI требуется повторный поиск активных callsite.
|
||
|
||
### Реестр расхождений этого источника
|
||
|
||
| Endpoint / schema | Runtime сейчас | OpenAPI сейчас | Целевой контракт | Действие backend | Блокирует frontend |
|
||
| ---------------------- | ----------------------------------- | ----------------------- | -------------------------------------- | ----------------------------- | ------------------ |
|
||
| Source cards | Источника нет | Enum/карточки нет | `sro-membership-check` | Добавить list/detail item | Да |
|
||
| Parser dashboard | Ответ разбирается вручную | `200` без schema | Typed `ParserDashboardResponse` | Добавить named schemas | Да |
|
||
| Source records | Source/group/payload отсутствуют | Enums отсутствуют | `sro_membership` + typed payload | Расширить enums/discriminator | Да |
|
||
| Record detail | Payload неизвестен | Generic/optional fields | Required organization + detail payload | Добавить named detail schema | Да |
|
||
| Refresh | Принимаются разные envelopes | Один `task_id` | `status` + canonical `task_ids` | Унифицировать runtime/OpenAPI | Да |
|
||
| Parser run | Ручной unknown response | `201` без schema | `ParserRunResponse` | Добавить schema | Для admin UI |
|
||
| Records pagination | `meta.pagination` ожидается runtime | `meta` нетипизирован | Полная pagination schema | Типизировать | Да |
|
||
| Export ticket/download | Ручные вызовы | Paths отсутствуют | Typed ticket/download schemas | Добавить в OpenAPI | Да |
|
||
| Upstream permission | Не представлено в UI/API | Error code отсутствует | `409 upstream_access_not_approved` | Добавить бизнес-ошибку | Да для refresh |
|
||
|
||
Известные общие расхождения (`GET /api/v1/sources/statuses/`, untyped parser auxiliaries,
|
||
разные `source_label/source_display`) не копируются в новый контракт.
|
||
|
||
## 3. Идентификаторы источника
|
||
|
||
| Идентификатор | Значение | Назначение |
|
||
| ---------------- | -------------------------------------- | ------------------------------------- |
|
||
| `sourceId` | `sro-membership-check` | ID спецификации |
|
||
| `parentSlug` | `sro-membership-check` | source list/detail/refresh |
|
||
| `routeSlug` | `sro-membership-check` | frontend route |
|
||
| `parserSource` | `sro_membership_check` | dashboard, parser run, jobs и logs |
|
||
| `sourceGroup` | `sro_membership` | records, organization detail и export |
|
||
| `recordType` | `sro_membership` | discriminator одной записи членства |
|
||
| `sourceItemCode` | `reestr_sro_membership` | единственный source item |
|
||
| `refreshKey` | `sro_membership_check` | manual/scheduled refresh |
|
||
| `taskName` | `parsers.sro_membership_check.refresh` | metadata фоновой задачи |
|
||
| `tableKey` | `sro-memberships` | frontend table/detail route |
|
||
|
||
Согласованные aliases: отсутствуют.
|
||
|
||
Правила идентичности:
|
||
|
||
- record `uid` — стабильный UUID, отличный от `organization.uid`;
|
||
- стабильная запись определяется парой organization + resolved SRO membership;
|
||
- upstream SRO ID хранится как string или integer без использования в URL внутреннего API;
|
||
- повторный batch для того же членства сохраняет record `uid`;
|
||
- изменение статуса `active → excluded` обновляет/версионирует ту же предметную запись, а не
|
||
создаёт второе членство;
|
||
- одновременные разные СРО создают разные record `uid`;
|
||
- короткое имя СРО не используется как единственный ключ из-за возможных совпадений.
|
||
|
||
## 4. Обязательный контракт организации
|
||
|
||
| Пользовательское поле | API-поле | Тип | Required | Nullable | Источник |
|
||
| --------------------- | ------------------------ | -------- | -------: | -------: | ------------------------------------ |
|
||
| Организация | `organization.name` | `string` | Да | Нет | Краткое наименование `reestr-sro.ru` |
|
||
| Полное наименование | `organization.full_name` | `string` | Да | Нет | Полное наименование `reestr-sro.ru` |
|
||
| ИНН | `organization.inn` | `string` | Да | Нет | SSR result |
|
||
| ОГРН | `organization.ogrn` | `string` | Да | Нет | SSR result |
|
||
| ОКПО | `organization.okpo` | `string` | Да | Нет | Backend enrichment |
|
||
| Каноническая связь | `organization.uid` | UUID | Да | Нет | Внутренний реестр организаций |
|
||
|
||
Минимальный объект:
|
||
|
||
```json
|
||
{
|
||
"organization": {
|
||
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
|
||
"name": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"full_name": "АВТОНОМНАЯ НЕКОММЕРЧЕСКАЯ ОРГАНИЗАЦИЯ \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"inn": "7728424294",
|
||
"ogrn": "1187700006273",
|
||
"okpo": "28213364"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Enrichment и публикация
|
||
|
||
1. Exact lookup по валидному ОГРН.
|
||
2. Если ОГРН отсутствует/неоднозначен — exact lookup по ИНН с проверкой названия.
|
||
3. ОКПО берётся только из доверенного внутреннего канонического реестра.
|
||
4. Fuzzy-only match не публикуется автоматически.
|
||
5. Несовпадение ИНН и ОГРН → quarantine `organization_identifier_conflict`.
|
||
6. Не найден ОКПО → quarantine `required_okpo_missing`.
|
||
7. Нельзя копировать ОГРН/ИНН в ОКПО или возвращать placeholder.
|
||
|
||
### Payload членства
|
||
|
||
List schema:
|
||
|
||
```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
|
||
}
|
||
```
|
||
|
||
Detail добавляет:
|
||
|
||
```ts
|
||
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'
|
||
}
|
||
}
|
||
```
|
||
|
||
`sro_url` и `source_record_url` проходят backend allowlist. API не возвращает URL другого
|
||
домена даже если он присутствовал на странице.
|
||
|
||
## 5. Матрица endpoint и решение по реализации
|
||
|
||
| № | Endpoint | Решение | Изменение |
|
||
| --: | ----------------------------------------------------------- | ---------------------- | ---------------------------------------- |
|
||
| 1 | `GET /api/v1/sources/` | Расширить | Карточка источника |
|
||
| 2 | `GET /api/v1/sources/{slug}/` | Расширить | Detail, source item, task/load |
|
||
| 3 | `GET /api/v1/parsers/dashboard/` | Расширить/типизировать | Source counts, schedule, coverage |
|
||
| 4 | `GET /api/v2/organization-source-records/` | Расширить | Enums, filters, ordering, list payload |
|
||
| 5 | `GET /api/v2/organization-source-records/{uid}/` | Расширить | Detail discriminator/payload |
|
||
| 6 | `POST /api/v1/sources/{slug}/refresh/` | Расширить | Slug, canonical task response, typed 409 |
|
||
| 7 | `POST /api/v1/parsers/run/{source_key}/` | Расширить | Admin key и typed response |
|
||
| 8 | `GET /api/v1/jobs/{task_id}/` | Использовать/расширить | Progress metadata |
|
||
| 9 | `POST /api/v1/parsers/upload/{source_key}/` | Не требуется | HTTP/SSR источник; typed 405 |
|
||
| 10 | `GET /api/v1/system/logs/` | Расширить | Source enum/filter |
|
||
| 11 | `GET /api/v1/system/logs/{id}/` | Расширить | Counts и errors |
|
||
| 12 | `GET /api/v1/system/logs/export/` | Расширить | Source в CSV |
|
||
| 13 | `POST /api/v2/organization-source-records/export-ticket/` | Описать в OpenAPI | Группа `sro_membership` |
|
||
| 14 | `POST /api/v2/organization-source-records/export-download/` | Описать в OpenAPI | Ticket download |
|
||
| 15 | `POST /api/v2/organization-source-records/export/` | Не использовать UI | Legacy sync endpoint |
|
||
|
||
Не использовать для нового frontend:
|
||
|
||
- `GET /api/v1/sources/statuses/` — активного read callsite нет;
|
||
- `GET /api/v1/parsers/sources/` — регистрация parser является backend concern;
|
||
- `GET/POST /api/v1/parsers/schedules/*` — frontend получает schedule через dashboard;
|
||
- `GET /api/v1/parsers/load-logs/` — история идёт через system logs;
|
||
- `GET /api/v1/parsers/records/` — records идут через v2;
|
||
- legacy parser results/download URL;
|
||
- `GET /api/v1/jobs/` — frontend poll только конкретный job;
|
||
- `POST /api/v1/jobs/{task_id}/control/` и `GET /api/v1/jobs/{task_id}/stream/`;
|
||
- прямой frontend proxy к `reestr-sro.ru`.
|
||
|
||
## 6. Общий паттерн описания endpoint
|
||
|
||
- internal base path: `/api`;
|
||
- auth: Bearer token по общей модели приложения;
|
||
- JSON: `application/json; charset=utf-8`;
|
||
- datetime: ISO 8601 с timezone;
|
||
- date: `YYYY-MM-DD`;
|
||
- identifiers: strings, кроме внутреннего numeric log ID;
|
||
- pagination: `page >= 1`, `page_size` из allowlist приложения;
|
||
- ordering: только перечисленные backend поля;
|
||
- empty collection: `[]`;
|
||
- missing optional: `null`;
|
||
- business error:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"errors": [
|
||
{
|
||
"code": "validation_error",
|
||
"message": "Некорректное значение параметра source_group.",
|
||
"field": "source_group"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Общие коды: `400` validation, `401` unauthenticated, `403` forbidden, `404` missing record,
|
||
`409` state conflict, `429` internal API throttle, `500` internal error, `503` source
|
||
temporarily unavailable. Upstream HTTP codes не проксируются frontend как есть.
|
||
|
||
Пагинация:
|
||
|
||
```json
|
||
{
|
||
"meta": {
|
||
"pagination": {
|
||
"page": 1,
|
||
"page_size": 50,
|
||
"pages": 2,
|
||
"total": 74,
|
||
"has_next": true,
|
||
"has_previous": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 7. Каталог источников
|
||
|
||
### 7.1 `GET /api/v1/sources/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/sources/?page=1&page_size=50&search=СРО
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
Query: `page`, `page_size`, `search`, общий `status`. Source-specific params отсутствуют.
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"slug": "sro-membership-check",
|
||
"title": "Проверка членства в СРО",
|
||
"description": "Сведения о действующем и прекращённом членстве организаций в СРО",
|
||
"status": "active",
|
||
"status_label": "Обновлено",
|
||
"last_updated_at": "2026-08-25T12:00:00Z",
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"source_items_count": 1
|
||
}
|
||
],
|
||
"meta": {
|
||
"pagination": {
|
||
"page": 1,
|
||
"page_size": 50,
|
||
"pages": 1,
|
||
"total": 1,
|
||
"has_next": false,
|
||
"has_previous": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Required: все поля карточки, кроме `last_updated_at`, который nullable до первого successful
|
||
batch. `records_count` — membership records; `organizations_count` — distinct
|
||
`organization.uid`. Counts всегда integers `>= 0`.
|
||
|
||
Errors: `401`, `403`, `400` для query. Нулевой список — `200`, не `404`.
|
||
|
||
### 7.2 `GET /api/v1/sources/{slug}/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/sources/sro-membership-check/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": {
|
||
"slug": "sro-membership-check",
|
||
"title": "Проверка членства в СРО",
|
||
"description": "Сведения о действующем и прекращённом членстве организаций в СРО",
|
||
"status": "active",
|
||
"status_label": "Обновлено",
|
||
"last_updated_at": "2026-08-25T12:00:00Z",
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"source_items": [
|
||
{
|
||
"code": "reestr_sro_membership",
|
||
"title": "Проверка членства в СРО",
|
||
"parser_source": "sro_membership_check",
|
||
"refresh_key": "sro_membership_check",
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"last_updated_at": "2026-08-25T12:00:00Z"
|
||
}
|
||
],
|
||
"latest_success_load": {
|
||
"id": 8712,
|
||
"status": "success",
|
||
"started_at": "2026-08-25T10:00:00Z",
|
||
"finished_at": "2026-08-25T12:00:00Z",
|
||
"meta": {
|
||
"candidate_organizations_count": 1400,
|
||
"queried_organizations_count": 1398,
|
||
"not_found_organizations_count": 366,
|
||
"raw_memberships_count": 1252,
|
||
"published_records_count": 1248,
|
||
"quarantined_records_count": 4,
|
||
"missing_admission_dates_count": 7,
|
||
"upstream_registry_date": "2026-08-15"
|
||
}
|
||
},
|
||
"active_task": null
|
||
}
|
||
}
|
||
```
|
||
|
||
`active_task` nullable; если есть, содержит `task_id`, `status`, `progress`, `started_at`.
|
||
`latest_success_load` nullable до первой публикации. Sum raw/published/quarantine не
|
||
подменяется organization count.
|
||
|
||
Errors: `404 source_not_found`, `401`, `403`.
|
||
|
||
## 8. Dashboard парсеров
|
||
|
||
### 8.1 `GET /api/v1/parsers/dashboard/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/parsers/dashboard/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"sources": [
|
||
{
|
||
"source": "sro_membership_check",
|
||
"source_display": "Проверка членства в СРО",
|
||
"status": "success",
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"last_updated_at": "2026-08-25T12:00:00Z",
|
||
"schedule": {
|
||
"enabled": true,
|
||
"kind": "incremental",
|
||
"timezone": "Europe/Moscow",
|
||
"next_run_at": "2026-08-26T01:00:00Z"
|
||
},
|
||
"active_task": null
|
||
}
|
||
],
|
||
"registry_enrichment_analytics": {
|
||
"population": {
|
||
"active_registry_organizations": 15000
|
||
},
|
||
"source_coverage": [
|
||
{
|
||
"source": "sro_membership_check",
|
||
"source_display": "Проверка членства в СРО",
|
||
"organizations_count": 612,
|
||
"coverage_percent": 4.08
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Named schema: `ParserDashboardResponse`. Для аналитики организаций ОПК denominator всегда
|
||
`population.active_registry_organizations`, numerator —
|
||
`source_coverage[].organizations_count`. Он равен distinct организациям ОПК с опубликованным
|
||
членством, а не всем queried candidates.
|
||
|
||
`/sources[].organizations_count` — все уникальные организации источника; это значение не
|
||
обязано совпадать с coverage организаций ОПК. `coverage_percent` backend вычисляет и округляет
|
||
по единому правилу проекта.
|
||
|
||
Errors: `401`, `403`, `500`. Частичное отсутствие одного источника не делает массив `sources`
|
||
нетипизированным.
|
||
|
||
## 9. Записи источника
|
||
|
||
### 9.1 `GET /api/v2/organization-source-records/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v2/organization-source-records/?source_group=sro_membership&source=sro_membership_check&record_type=sro_membership&page=1&page_size=50&search=7728424294&membership_status=active&ordering=organization__name,sro_name,admission_date
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
Поддерживаемые query params:
|
||
|
||
| Параметр | Тип | Правило |
|
||
| --------------------- | --------------- | ------------------------------------------ | ------------ |
|
||
| `source_group` | enum | Для этой таблицы required `sro_membership` |
|
||
| `source` | enum | `sro_membership_check` |
|
||
| `record_type` | enum | `sro_membership` |
|
||
| `organization` | UUID | Фильтр карточки организации |
|
||
| `search` | string <= 200 | name/full_name/ИНН/ОГРН/ОКПО/SRO name |
|
||
| `membership_status` | `active | excluded` | Exact filter |
|
||
| `region` | string | Exact normalized source region |
|
||
| `sro_id` | string | Exact resolved ID |
|
||
| `admission_date_from` | date | Inclusive |
|
||
| `admission_date_to` | date | Inclusive |
|
||
| `ordering` | comma-separated | Поля из allowlist, `-` для descending |
|
||
| `page`, `page_size` | integer | Общая пагинация |
|
||
|
||
Ordering allowlist: `organization__name`, `organization__inn`, `organization__ogrn`,
|
||
`organization__okpo`, `organization__full_name`, `membership_status`, `region`, `sro_name`,
|
||
`admission_date`, `updated_at`. Default:
|
||
`organization__name,sro_name,admission_date`.
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"uid": "b2d9c7d0-8f96-5e83-9df1-9a383f5ccf4d",
|
||
"source_group": "sro_membership",
|
||
"source": "sro_membership_check",
|
||
"record_type": "sro_membership",
|
||
"title": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\" — СРО Ассоциация «НЕФТЕГАЗСЕРВИС»",
|
||
"status": "active",
|
||
"record_date": "2023-12-11",
|
||
"organization": {
|
||
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
|
||
"name": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"full_name": "АВТОНОМНАЯ НЕКОММЕРЧЕСКАЯ ОРГАНИЗАЦИЯ \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"inn": "7728424294",
|
||
"ogrn": "1187700006273",
|
||
"okpo": "28213364"
|
||
},
|
||
"payload": {
|
||
"membership_status": "active",
|
||
"membership_status_raw": "Является членом",
|
||
"region": "Московская область",
|
||
"sro_id": "517",
|
||
"sro_name": "СРО Ассоциация «НЕФТЕГАЗСЕРВИС»",
|
||
"sro_url": "https://moskva.reestr-sro.ru/sro-v-proektirovanii/sro-id-517/",
|
||
"admission_date": "2023-12-11",
|
||
"source_registry_date": "2026-08-15"
|
||
},
|
||
"updated_at": "2026-08-25T12:00:00Z"
|
||
}
|
||
],
|
||
"meta": {
|
||
"pagination": {
|
||
"page": 1,
|
||
"page_size": 50,
|
||
"pages": 1,
|
||
"total": 1,
|
||
"has_next": false,
|
||
"has_previous": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
List остаётся лёгким: raw HTML, lineage и error samples в нём отсутствуют. `record_date`
|
||
равен `admission_date` или `null`; дата загрузки не используется как fallback.
|
||
|
||
Validation:
|
||
|
||
- `400 invalid_source_group`;
|
||
- `400 invalid_membership_status`;
|
||
- `400 invalid_ordering`;
|
||
- `400 invalid_date_range`;
|
||
- `200 data=[]` для отсутствия записей;
|
||
- `401`, `403` по общей модели.
|
||
|
||
### 9.2 `GET /api/v2/organization-source-records/{uid}/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v2/organization-source-records/b2d9c7d0-8f96-5e83-9df1-9a383f5ccf4d/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": {
|
||
"uid": "b2d9c7d0-8f96-5e83-9df1-9a383f5ccf4d",
|
||
"source_group": "sro_membership",
|
||
"source": "sro_membership_check",
|
||
"record_type": "sro_membership",
|
||
"title": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\" — СРО Ассоциация «НЕФТЕГАЗСЕРВИС»",
|
||
"status": "active",
|
||
"record_date": "2023-12-11",
|
||
"organization": {
|
||
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
|
||
"name": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"full_name": "АВТОНОМНАЯ НЕКОММЕРЧЕСКАЯ ОРГАНИЗАЦИЯ \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
|
||
"inn": "7728424294",
|
||
"ogrn": "1187700006273",
|
||
"okpo": "28213364"
|
||
},
|
||
"payload": {
|
||
"membership_status": "active",
|
||
"membership_status_raw": "Является членом",
|
||
"region": "Московская область",
|
||
"sro_id": "517",
|
||
"sro_name": "СРО Ассоциация «НЕФТЕГАЗСЕРВИС»",
|
||
"sro_url": "https://moskva.reestr-sro.ru/sro-v-proektirovanii/sro-id-517/",
|
||
"admission_date": "2023-12-11",
|
||
"source_registry_date": "2026-08-15",
|
||
"source_record_url": "https://www.reestr-sro.ru/proverka_dopuska/?q=7728424294",
|
||
"retrieved_at": "2026-08-25T11:58:00Z",
|
||
"admission_date_missing_reason": null,
|
||
"lineage": {
|
||
"lookup_key": "inn",
|
||
"lookup_value": "7728424294",
|
||
"sro_resolution": "row_link"
|
||
}
|
||
},
|
||
"batch": {
|
||
"id": 8712,
|
||
"source_version": "registry-date:2026-08-15",
|
||
"published_at": "2026-08-25T12:00:00Z"
|
||
},
|
||
"created_at": "2026-08-25T12:00:00Z",
|
||
"updated_at": "2026-08-25T12:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
Required/nullable:
|
||
|
||
- `admission_date`, `source_registry_date`, `admission_date_missing_reason` nullable;
|
||
- `sro_url`, `sro_id`, `sro_name`, `region`, organization quartet required;
|
||
- если `admission_date` non-null, missing reason обязан быть null;
|
||
- если дата null, missing reason обязан быть non-null;
|
||
- `source_record_url` и `sro_url` должны пройти same-site allowlist.
|
||
|
||
Errors: `400 invalid_uid`, `404 source_record_not_found`, `401`, `403`.
|
||
|
||
## 10. Refresh и фоновые задачи
|
||
|
||
### 10.1 `POST /api/v1/sources/{slug}/refresh/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
POST /api/v1/sources/sro-membership-check/refresh/
|
||
Content-Type: application/json
|
||
Authorization: Bearer <token>
|
||
|
||
{"params":{}}
|
||
```
|
||
|
||
Frontend не отправляет upstream URL, cookies, query templates или organization IDs.
|
||
|
||
**Response `202`**
|
||
|
||
```json
|
||
{
|
||
"status": "queued",
|
||
"task_ids": ["8f6697a4-9dd1-43a7-b22a-35c91457bd70"]
|
||
}
|
||
```
|
||
|
||
`task_ids` всегда non-empty array strings. Один источник сейчас создаёт одну orchestration
|
||
task, но envelope остаётся общим.
|
||
|
||
Errors:
|
||
|
||
- `403 refresh_forbidden`;
|
||
- `404 source_not_found`;
|
||
- `409 refresh_already_running`;
|
||
- `409 upstream_access_not_approved` до разрешения автоматизированного доступа;
|
||
- `503 source_temporarily_unavailable` только если запуск не может быть поставлен в очередь.
|
||
|
||
### 10.2 `POST /api/v1/parsers/run/{source_key}/`
|
||
|
||
Административный эквивалент; обычный frontend использует 10.1.
|
||
|
||
**Request**
|
||
|
||
```http
|
||
POST /api/v1/parsers/run/sro_membership_check/
|
||
Content-Type: application/json
|
||
Authorization: Bearer <token>
|
||
|
||
{"mode":"incremental"}
|
||
```
|
||
|
||
`mode`: `incremental|full`; право на `full` может быть только у администратора.
|
||
|
||
**Response `201`**
|
||
|
||
```json
|
||
{
|
||
"source": "sro_membership_check",
|
||
"status": "queued",
|
||
"task_ids": ["8f6697a4-9dd1-43a7-b22a-35c91457bd70"]
|
||
}
|
||
```
|
||
|
||
Errors совпадают с 10.1 плюс `400 invalid_mode` и `403 full_refresh_forbidden`.
|
||
|
||
### 10.3 `GET /api/v1/jobs/{task_id}/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/jobs/8f6697a4-9dd1-43a7-b22a-35c91457bd70/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"task_id": "8f6697a4-9dd1-43a7-b22a-35c91457bd70",
|
||
"task_name": "parsers.sro_membership_check.refresh",
|
||
"source": "sro_membership_check",
|
||
"status": "running",
|
||
"progress": 48,
|
||
"created_at": "2026-08-25T10:00:00Z",
|
||
"started_at": "2026-08-25T10:00:03Z",
|
||
"finished_at": null,
|
||
"message": "Проверено 672 из 1400 организаций",
|
||
"result": null,
|
||
"meta": {
|
||
"mode": "incremental",
|
||
"candidate_organizations_count": 1400,
|
||
"queried_organizations_count": 672,
|
||
"found_memberships_count": 603,
|
||
"published_records_count": 0,
|
||
"quarantined_records_count": 2
|
||
}
|
||
}
|
||
```
|
||
|
||
Terminal `result`:
|
||
|
||
```json
|
||
{
|
||
"batch_id": 8712,
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"upstream_registry_date": "2026-08-15"
|
||
}
|
||
```
|
||
|
||
Status enum: `queued`, `running`, `retrying`, `success`, `failed`, `cancelled`, `skipped`.
|
||
`progress` integer `0..100`. Errors: `404 job_not_found`, `401`, `403`.
|
||
|
||
### 10.4 `POST /api/v1/parsers/upload/{source_key}/` (условный)
|
||
|
||
Не требуется: источник не принимает пользовательский файл.
|
||
|
||
**Request**, который не должен использовать frontend:
|
||
|
||
```http
|
||
POST /api/v1/parsers/upload/sro_membership_check/
|
||
Content-Type: multipart/form-data
|
||
```
|
||
|
||
**Response `405`**
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"errors": [
|
||
{
|
||
"code": "upload_not_supported",
|
||
"message": "Источник обновляется через backend HTTP integration.",
|
||
"field": null
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Backend может не регистрировать source key в generic upload router; OpenAPI должен явно
|
||
показывать, что upload для этого source недоступен.
|
||
|
||
## 11. История обновлений
|
||
|
||
### 11.1 `GET /api/v1/system/logs/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/system/logs/?source=sro_membership_check&page=1&page_size=25&ordering=-started_at
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
Фильтры: `source`, `status`, `batch_id: integer`, dates, search, pagination, ordering.
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"id": 8712,
|
||
"batch_id": 8712,
|
||
"source": "sro_membership_check",
|
||
"source_display": "Проверка членства в СРО",
|
||
"status": "success",
|
||
"started_at": "2026-08-25T10:00:00Z",
|
||
"finished_at": "2026-08-25T12:00:00Z",
|
||
"records_count": 1248,
|
||
"organizations_count": 1032,
|
||
"error_count": 0
|
||
}
|
||
],
|
||
"meta": {
|
||
"pagination": {
|
||
"page": 1,
|
||
"page_size": 25,
|
||
"pages": 1,
|
||
"total": 1,
|
||
"has_next": false,
|
||
"has_previous": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`batch_id` integer во list, detail и export. Empty result — `200`.
|
||
|
||
### 11.2 `GET /api/v1/system/logs/{id}/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/system/logs/8712/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**
|
||
|
||
```json
|
||
{
|
||
"data": {
|
||
"id": 8712,
|
||
"batch_id": 8712,
|
||
"source": "sro_membership_check",
|
||
"source_display": "Проверка членства в СРО",
|
||
"status": "success",
|
||
"started_at": "2026-08-25T10:00:00Z",
|
||
"finished_at": "2026-08-25T12:00:00Z",
|
||
"meta": {
|
||
"candidate_organizations_count": 1400,
|
||
"queried_organizations_count": 1398,
|
||
"not_found_organizations_count": 366,
|
||
"raw_memberships_count": 1252,
|
||
"published_records_count": 1248,
|
||
"quarantined_records_count": 4,
|
||
"missing_admission_dates_count": 7,
|
||
"http_errors_count": 2,
|
||
"parse_errors_count": 0,
|
||
"upstream_registry_date": "2026-08-15"
|
||
},
|
||
"errors": []
|
||
}
|
||
}
|
||
```
|
||
|
||
`errors[]` содержит ограниченные samples: code, stage, safe URL/path, organization UID,
|
||
message. Cookies, response body и секреты не включаются.
|
||
|
||
### 11.3 `GET /api/v1/system/logs/export/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
GET /api/v1/system/logs/export/?source=sro_membership_check&status=success
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**Response `200`**: `text/csv; charset=utf-8`, `Content-Disposition: attachment`.
|
||
|
||
CSV включает list-поля и counts detail metadata. Ошибки: `400`, `401`, `403`, `500`.
|
||
Response binary schema должен присутствовать в OpenAPI; не использовать JSON DTO.
|
||
|
||
## 12. Выгрузка данных источника
|
||
|
||
Canonical frontend flow: ticket → download. Export читает только published normalized rows.
|
||
|
||
### 12.1 `POST /api/v2/organization-source-records/export-ticket/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
POST /api/v2/organization-source-records/export-ticket/
|
||
Content-Type: application/json
|
||
Authorization: Bearer <token>
|
||
|
||
{
|
||
"source_groups": ["sro_membership"],
|
||
"format": "xlsx",
|
||
"filters": {
|
||
"membership_status": ["active", "excluded"]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response `202`**
|
||
|
||
```json
|
||
{
|
||
"ticket": "exp_01K38SROMEMBERSHIP",
|
||
"status": "queued",
|
||
"expires_at": "2026-08-25T13:00:00Z"
|
||
}
|
||
```
|
||
|
||
Formats: `json`, `csv`, `xlsx`. Ошибки: invalid group/format/filter, forbidden, too large.
|
||
|
||
### 12.2 `POST /api/v2/organization-source-records/export-download/`
|
||
|
||
**Request**
|
||
|
||
```http
|
||
POST /api/v2/organization-source-records/export-download/
|
||
Content-Type: application/json
|
||
Authorization: Bearer <token>
|
||
|
||
{"ticket":"exp_01K38SROMEMBERSHIP"}
|
||
```
|
||
|
||
**Response `200`**: binary с точным MIME для выбранного формата и безопасным filename.
|
||
|
||
`202 export_not_ready` возвращает JSON status/retry_after; `404 export_ticket_not_found`,
|
||
`410 export_ticket_expired`, `403` и `500` типизированы.
|
||
|
||
Колонки export: record UID, Организация, ИНН, ОГРН, ОКПО, Полное наименование, Статус,
|
||
исходный статус, Регион, SRO ID, СРО, SRO URL, Дата допуска, дата версии реестра, timestamps.
|
||
URL экспортируется как string, не HTML.
|
||
|
||
### 12.3 `POST /api/v2/organization-source-records/export/`
|
||
|
||
Legacy sync endpoint не используется новым frontend.
|
||
|
||
**Request**
|
||
|
||
```json
|
||
{ "source_group": "sro_membership", "format": "xlsx" }
|
||
```
|
||
|
||
**Response**: если endpoint сохраняется для обратной совместимости, schema и binary MIME должны
|
||
совпадать с runtime; для нового сценария backend отвечает `400 use_ticket_export` либо
|
||
документирует deprecation. Нельзя одновременно считать sync и ticket flow каноническими.
|
||
|
||
## 13. Статусы и жизненный цикл
|
||
|
||
| Область | Активные | Terminal |
|
||
| -------------- | ------------------------------- | ------------------------------------------- |
|
||
| Job | `queued`, `running`, `retrying` | `success`, `failed`, `cancelled`, `skipped` |
|
||
| Source card | `refreshing` | `active`, `error`, `unavailable` |
|
||
| UI operational | `Выполняется` | `Обновлено` |
|
||
| Membership | `active` | `excluded` |
|
||
|
||
UI label `Обновлено` не означает success конкретной task; история показывает точный terminal
|
||
status. Membership status не влияет на parser status.
|
||
|
||
Publication:
|
||
|
||
```text
|
||
queued → running → collect/normalize/enrich → validate complete batch
|
||
→ atomically publish → success
|
||
```
|
||
|
||
При failure последний successful snapshot остаётся читаемым. `not_found` organization lookup
|
||
учитывается как coverage result, а не exception. Partial batch может сохраниться для
|
||
диагностики, но не становится current.
|
||
|
||
## 14. Согласованность данных между endpoint
|
||
|
||
| Инвариант | Требование |
|
||
| ---------------------- | --------------------------------------------------- |
|
||
| Title | Ровно `Проверка членства в СРО` |
|
||
| Slug/source/group | Значения раздела 3 без aliases |
|
||
| `records_count` | Published membership records |
|
||
| `organizations_count` | Distinct `organization.uid` среди published records |
|
||
| Coverage | Distinct active-registry organizations with records |
|
||
| `last_updated_at` | Publication time последнего successful batch |
|
||
| `source_registry_date` | Дата из upstream HTML, не publication time |
|
||
| Organization quartet | Одинаков во list/detail/export |
|
||
| Membership UID | Стабилен между batches и endpoint |
|
||
| Admission date | Одинакова в payload, `record_date`, detail/export |
|
||
| SRO URL | HTTPS + same-site allowlist |
|
||
| Pagination total | После всех server filters |
|
||
| Status | Generic record status согласован с payload enum |
|
||
|
||
Одна организация с двумя СРО увеличивает `records_count` на 2 и
|
||
`organizations_count` на 1.
|
||
|
||
## 15. Ошибки, доступ, производительность и ограничения
|
||
|
||
### Матрица доступа
|
||
|
||
| Операция | Read user | Operator | Admin |
|
||
| ---------------------- | ------------------------- | -------- | ----- |
|
||
| Source cards/dashboard | Да | Да | Да |
|
||
| Records list/detail | Да | Да | Да |
|
||
| Refresh incremental | Нет | Да | Да |
|
||
| Full parser run | Нет | Нет | Да |
|
||
| History read/export | По общей политике проекта |
|
||
| Records export | По общей политике проекта |
|
||
|
||
### Source-specific ошибки
|
||
|
||
| Code | HTTP | Когда |
|
||
| -------------------------------- | ---: | --------------------------------------------------------- |
|
||
| `upstream_access_not_approved` | 409 | Автосбор ещё не разрешён владельцем сайта |
|
||
| `refresh_already_running` | 409 | Активная task того же источника |
|
||
| `source_temporarily_unavailable` | 503 | Нельзя поставить refresh в очередь |
|
||
| `invalid_membership_status` | 400 | Неизвестный filter enum |
|
||
| `unsafe_sro_url` | 500 | Нарушен published invariant; запись не отдаётся как valid |
|
||
| `source_record_not_found` | 404 | Нет внутренней published записи |
|
||
|
||
Upstream errors фиксируются в job/log metadata; read endpoint продолжает отдавать последний
|
||
successful snapshot.
|
||
|
||
### Нефункциональные требования
|
||
|
||
- list p95 <= 800 ms для page size 50 без учёта network клиента;
|
||
- detail p95 <= 500 ms;
|
||
- индексы: source_group/source/record_type, organization UID, status, region, SRO ID,
|
||
admission date, normalized search fields;
|
||
- N+1 enrichment в read path запрещён;
|
||
- list не содержит lineage/raw HTML;
|
||
- export выполняется асинхронно;
|
||
- backend external adapter соблюдает минимум `Crawl-delay: 3` и более строгие условия
|
||
разрешения владельца сайта;
|
||
- hostname allowlist проверяется после каждого redirect;
|
||
- response body upstream имеет size limit, timeout и schema-drift detection;
|
||
- raw HTML не попадает в application logs;
|
||
- URL query с ИНН не должен попадать в telemetry без redaction policy.
|
||
|
||
`robots.txt` запрещает query/search URLs. До документированного разрешения production task не
|
||
должна выполнять внешние запросы; contract refresh остаётся реализуемым и возвращает typed 409.
|
||
|
||
## 16. Требования к OpenAPI и generated-клиенту
|
||
|
||
Backend обязан:
|
||
|
||
1. добавить canonical enums `sro-membership-check`, `sro_membership_check`,
|
||
`sro_membership`, `sro_membership` record type;
|
||
2. описать required/nullable каждого поля;
|
||
3. создать named schemas `SroMembershipListPayload`, `SroMembershipDetailPayload`,
|
||
organization summary, dashboard, refresh и errors;
|
||
4. типизировать `meta.pagination` полностью;
|
||
5. типизировать `task_ids` как non-empty array strings;
|
||
6. описать binary MIME history/export responses;
|
||
7. добавить ticket/download paths в `openapi.json`;
|
||
8. унифицировать `source_display` и `batch_id: integer`;
|
||
9. не использовать `additionalProperties: true` вместо payload schema;
|
||
10. добавить examples, совпадающие с runtime и contract fixtures.
|
||
|
||
После публикации schema frontend выполняет:
|
||
|
||
```bash
|
||
bun run apigen
|
||
bun run test:contract
|
||
```
|
||
|
||
Diff проверяется в `src/shared/api/generated-api/` и `src/shared/model/generated-zod/`.
|
||
Ручные DTO удаляются только после совпадения runtime/OpenAPI.
|
||
|
||
## 17. Backend-тесты и acceptance criteria
|
||
|
||
### Минимальные contract fixtures
|
||
|
||
1. Active membership `7728424294` с кликабельной СРО и датой допуска.
|
||
2. Excluded membership.
|
||
3. Одна организация с двумя membership rows.
|
||
4. Zero result без ошибки.
|
||
5. Nullable admission date + обязательная missing reason.
|
||
6. Missing/unsafe SRO URL → quarantine, не valid response.
|
||
7. Missing OKPO → quarantine.
|
||
8. Identifier conflict ИНН/ОГРН.
|
||
9. Unknown status/schema drift.
|
||
10. Failed batch сохраняет предыдущий snapshot.
|
||
|
||
### Минимальная матрица backend/contract tests
|
||
|
||
- [ ] Source list/detail schemas и counts.
|
||
- [ ] Dashboard named schema, source coverage и denominator.
|
||
- [ ] Records filters, ordering, pagination и empty result.
|
||
- [ ] List/detail required/nullable payload rules.
|
||
- [ ] Multiple SRO rows для одной organization UID.
|
||
- [ ] Stable record UID между batches.
|
||
- [ ] Status normalization active/excluded.
|
||
- [ ] OKPO enrichment и quarantine.
|
||
- [ ] Same-site URL allowlist и redirect validation.
|
||
- [ ] Refresh canonical `task_ids`.
|
||
- [ ] Typed `upstream_access_not_approved`.
|
||
- [ ] Jobs status/progress/result.
|
||
- [ ] History list/detail/CSV.
|
||
- [ ] Export ticket/download formats и expiry.
|
||
- [ ] Auth/permissions по endpoint.
|
||
- [ ] OpenAPI examples проходят runtime schema validation.
|
||
|
||
### Backend acceptance criteria
|
||
|
||
- [ ] Все выбранные endpoint реализованы и описаны в OpenAPI.
|
||
- [ ] Frontend не нуждается в source-specific read endpoint.
|
||
- [ ] Каждая published запись содержит Организация/Наименование, ИНН, ОГРН, ОКПО.
|
||
- [ ] Полное наименование, статус, регион, СРО и дата допуска соответствуют payload schema.
|
||
- [ ] Одна организация может иметь несколько stable membership records.
|
||
- [ ] Source card, dashboard, records, history и export согласованы по counts/dates.
|
||
- [ ] Runtime, OpenAPI, generated client и contract tests совпадают.
|
||
- [ ] Запуск внешнего сбора закрыт typed 409 до upstream approval.
|
||
- [ ] `bun run test:contract` проходит на интеграционном стенде.
|
||
|
||
## 18. Чего backend не должен возвращать
|
||
|
||
- upstream HTML, DOM selectors или JavaScript;
|
||
- cookies, XSRF/session values и response headers внешнего сайта;
|
||
- данные НОПРИЗ, ФНС, сайтов СРО или иных доменов как данные этого источника;
|
||
- HTML `<a>` вместо `sro_name` + `sro_url`;
|
||
- ОКПО, скопированный из другого идентификатора;
|
||
- пустые strings/`—` вместо `null`;
|
||
- дату загрузки вместо отсутствующей даты допуска;
|
||
- небезопасный или не-same-site URL;
|
||
- raw traceback/upstream response body в frontend error;
|
||
- UI labels, цвета или готовую разметку;
|
||
- неполный batch как current snapshot;
|
||
- разные aliases идентификаторов в разных endpoint.
|
||
|
||
## 19. Решения и открытые вопросы
|
||
|
||
| Вопрос | Ответственный | Решение |
|
||
| ---------------------------------- | --------------------- | ---------------------------------------------------------------------------------- |
|
||
| Автоматизированный доступ | Product/Legal/Backend | До разрешения refresh возвращает `409 upstream_access_not_approved` |
|
||
| Реализация внешнего parser | Backend | Вне этого документа; обязана удовлетворять observable contract и contract fixtures |
|
||
| ОКПО | Backend | Exact enrichment из внутреннего канонического реестра; иначе quarantine |
|
||
| SRO link отсутствует в строке | Backend | Только same-site ID/sitemap resolution; другие сайты запрещены |
|
||
| Дата допуска отсутствует | Product/Backend | `null` + typed missing reason; frontend показывает `—` |
|
||
| Full sweep schedule | Product/Backend | Ежемесячно после upstream registry date, incremental ежедневно |
|
||
| Raw retention | Backend/ИБ | Не менее 90 дней и 10 successful batches |
|
||
| Схема неизвестного upstream status | Backend/Frontend | Quarantine и контрактное расширение enum, без silent fallback |
|