59 KiB
template_id, template_version, source_id, source_name, document_status
| template_id | template_version | source_id | source_name | document_status |
|---|---|---|---|---|
| source-backend-api | 1 | sro-membership-check | Проверка членства в СРО | review |
Backend API источника «Проверка членства в СРО»
Уточнение 14.09.2026: разделы подготовки выгрузки с
preparing/ HTTP 202 заменены общим runtime-контрактом: готовый ticket HTTP 201 → native form download HTTP 200; устаревшее поколение — HTTP 503source_export_not_ready, истёкший/использованный ticket — HTTP 410ticket_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:
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 |
Дата допуска берётся со страницы той же СРО:
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:
- backend возвращает нормализованные записи через универсальные endpoint;
- ни один frontend endpoint не выдаёт upstream HTML;
- ОКПО и
organization.uidуже разрешены; - СРО имеет безопасный same-site URL;
- дата допуска —
date | nullс явной причиной отсутствия в metadata; - zero result отличается от ошибки получения/парсинга;
- 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 | Да | Нет | Внутренний реестр организаций |
Минимальный объект:
{
"organization": {
"uid": "0ebfa744-efc0-4e91-afaf-6b784b2bb22f",
"name": "АНО \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
"full_name": "АВТОНОМНАЯ НЕКОММЕРЧЕСКАЯ ОРГАНИЗАЦИЯ \"ИННОВАЦИОННЫЙ ИНЖИНИРИНГОВЫЙ ЦЕНТР\"",
"inn": "7728424294",
"ogrn": "1187700006273",
"okpo": "28213364"
}
}
Enrichment и публикация
- Exact lookup по валидному ОГРН.
- Если ОГРН отсутствует/неоднозначен — exact lookup по ИНН с проверкой названия.
- ОКПО берётся только из доверенного внутреннего канонического реестра.
- Fuzzy-only match не публикуется автоматически.
- Несовпадение ИНН и ОГРН → quarantine
organization_identifier_conflict. - Не найден ОКПО → quarantine
required_okpo_missing. - Нельзя копировать ОГРН/ИНН в ОКПО или возвращать placeholder.
Payload членства
List schema:
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 добавляет:
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:
{
"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 как есть.
Пагинация:
{
"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
GET /api/v1/sources/?page=1&page_size=50&search=СРО
Authorization: Bearer <token>
Query: page, page_size, search, общий status. Source-specific params отсутствуют.
Response 200
{
"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
GET /api/v1/sources/sro-membership-check/
Authorization: Bearer <token>
Response 200
{
"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
GET /api/v1/parsers/dashboard/
Authorization: Bearer <token>
Response 200
{
"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
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
{
"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
GET /api/v2/organization-source-records/b2d9c7d0-8f96-5e83-9df1-9a383f5ccf4d/
Authorization: Bearer <token>
Response 200
{
"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_reasonnullable;sro_url,sro_id,sro_name,region, organization quartet required;- если
admission_datenon-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
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
{
"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
POST /api/v1/parsers/run/sro_membership_check/
Content-Type: application/json
Authorization: Bearer <token>
{"mode":"incremental"}
mode: incremental|full; право на full может быть только у администратора.
Response 201
{
"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
GET /api/v1/jobs/8f6697a4-9dd1-43a7-b22a-35c91457bd70/
Authorization: Bearer <token>
Response 200
{
"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:
{
"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:
POST /api/v1/parsers/upload/sro_membership_check/
Content-Type: multipart/form-data
Response 405
{
"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
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
{
"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
GET /api/v1/system/logs/8712/
Authorization: Bearer <token>
Response 200
{
"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
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
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
{
"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
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
{ "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:
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 обязан:
- добавить canonical enums
sro-membership-check,sro_membership_check,sro_membership,sro_membershiprecord type; - описать required/nullable каждого поля;
- создать named schemas
SroMembershipListPayload,SroMembershipDetailPayload, organization summary, dashboard, refresh и errors; - типизировать
meta.paginationполностью; - типизировать
task_idsкак non-empty array strings; - описать binary MIME history/export responses;
- добавить ticket/download paths в
openapi.json; - унифицировать
source_displayиbatch_id: integer; - не использовать
additionalProperties: trueвместо payload schema; - добавить examples, совпадающие с runtime и contract fixtures.
После публикации schema frontend выполняет:
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
- Active membership
7728424294с кликабельной СРО и датой допуска. - Excluded membership.
- Одна организация с двумя membership rows.
- Zero result без ошибки.
- Nullable admission date + обязательная missing reason.
- Missing/unsafe SRO URL → quarantine, не valid response.
- Missing OKPO → quarantine.
- Identifier conflict ИНН/ОГРН.
- Unknown status/schema drift.
- 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 |