Files
mostovik-backend/docs/source-integration/sources/sro-membership-check/backend-api.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

59 KiB
Raw Blame History

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 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:

GET https://www.reestr-sro.ru/proverka_dopuska/
  ?q=<exact INN or OGRN>
  &sro_name=
  &sro_id=
  &sro_reg_number=
  &region_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:

  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 Да Нет Внутренний реестр организаций

Минимальный объект:

{
  "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:

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_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

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 обязан:

  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 выполняет:

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