Files
mostovik-backend/docs/sro-membership-source.md
Aleksandr Meshchryakov 2cfc057e34
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 2m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m48s
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 1m54s
fix: publish valid SRO memberships while preserving quarantined history
2026-09-15 13:25:43 +02:00

199 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Проверка членства в СРО: эксплуатация
Источник `sro_membership_check`, группа/тип `sro_membership`, карточка
`sro-membership-check`. Upstream — сайт `reestr-sro.ru`; внутренний справочник
организаций используется для точной привязки UID и ОКПО. Другие внешние реестры
загрузчик не опрашивает.
## Доступ и пределы запросов
По умолчанию сбор закрыт. Для production необходимы одновременно
`SRO_UPSTREAM_ACCESS_APPROVED=true` и непустой
`SRO_UPSTREAM_APPROVAL_REFERENCE` — ссылка/номер документированного разрешения
владельца сайта на автоматизированные query/search запросы. Согласование разработки
внутри проекта не заменяет разрешение владельца upstream. Условия разрешения
должны быть совместимы с настройками загрузчика; более строгие пределы задаются
перед включением. Секреты в approval reference хранить нельзя.
15.09.2026 пользователь явно изменил требование: «Изменить требование и включить
сбор на dev». Только для dev предусмотрен отдельный opt-in
`SRO_DEV_COLLECTION_ENABLED=true`. Он разрешает сбор без изменения
`SRO_UPSTREAM_ACCESS_APPROVED` и `SRO_UPSTREAM_APPROVAL_REFERENCE` и не обозначает
разрешение владельца upstream. По умолчанию флаг `false`; на production он должен
оставаться выключенным. Код разрешает запуск при включённом dev-флаге **или** при
наличии обоих параметров согласованного доступа. Для отключения dev-исключения
верните флаг в `false` и примените конфигурацию web/worker.
Исходный [source-first контракт](source-integration/sources/sro-membership-check/backend-api.md)
с ограничением production refresh и
[документ замечаний](source-integration/source-records-backend-improvements.md)
с исходным ответом `409` сохранены как происхождение требования. Настоящее
изменение относится только к включению сбора на dev; ограничения запросов,
привязки организаций и публикации данных сохраняются.
Оба ручных entrypoint проверяют этот gate до enqueue; task и HTTP клиент повторяют
проверку перед внешним IO. Закрытый gate возвращает typed
`409 upstream_access_not_approved`. Если gate закрыт после постановки,
job завершается с ошибкой, без успешной пустой публикации.
| Настройка | По умолчанию | Значение |
| --- | --- | --- |
| `SRO_DEV_COLLECTION_ENABLED` | `false` | Явное включение сбора только на dev, отдельно от подтверждения разрешения upstream. |
| `SRO_REQUEST_INTERVAL_SECONDS` | `3` | Минимум 3 секунды между запросами, включая redirects/retries; можно увеличить. |
| `SRO_HTTP_MAX_RESPONSE_BYTES` | `2097152` | Предел распакованного тела одного ответа. |
| `SRO_HTTP_TIMEOUT_SECONDS` | `30` | Read timeout; connect timeout 10 секунд. |
| `SRO_MAX_REQUESTS_PER_RUN` | `10000` | Общий предел запросов одного запуска, включая redirects/retries/sitemaps. |
HTTP клиент делает не более трёх попыток при транспортной ошибке и ответах
429/500/502/503/504. `Retry-After` учитывается как секунды или HTTP date, ожидание
свыше 300 секунд завершает запуск ошибкой. Цепочка redirects ограничена пятью
ответами; каждый адрес проверяется до обращения. Разрешён только HTTPS без
credentials/нестандартного порта, на `reestr-sro.ru` и его поддоменах. Переменные
окружения HTTP proxy автоматически не используются. HTML/query с идентификаторами
не выводится в application logs. User-Agent — нейтральный
`Mostovik SRO integration`, без утверждения о разрешении upstream.
Наблюдаемый 15.09.2026 ответ lookup `404` учитывается как отсутствие членства
только для `/proverka_dopuska/` с одним `q` из 10 или 13 цифр, при точном title
«Проверка членства в реестре СРО, проверить допуск организации в СРО по ИНН»,
единственном h1 «Запрашиваемая страница на сайте отсутствует.» и отсутствии
`table.sro-members`. Конечный `q` после redirects должен совпадать с запрошенным
идентификатором организации; иначе запуск отклоняется с
`sro_lookup_identifier_mismatch`. Другой HTML при lookup `404` отклоняется с
`sro_lookup_response_unrecognized`. На остальных путях `404` сохраняет прежнее
значение `sro_page_not_found`, в том числе на страницах даты допуска.
Этот шаблон `404` неоднозначен: он сам по себе не доказывает отсутствие членства.
Поэтому если **все** кандидаты получили такой ответ и ни один lookup не вернул
успешно разобранный `200`, весь запуск отклоняется с `sro_ambiguous_empty_scan`
до публикации и изменения checkpoints; прежние записи сохраняются. Смешанный
запуск с корректным `200` может учитывать распознанные отрицательные ответы.
Приватный raw manifest сохраняет `status_code`; artifact metadata содержит
`recognized_lookup_404_count` и `successful_lookup_200_count`. Предел размера
ответа действует и для lookup `404`. Дата реестра из error-шаблона не извлекается.
## Публикация и обновление
Celery task: `parsers.sro_membership_check.refresh`;
Python entrypoint: `apps.parsers.tasks_registry_snapshots.parse_sro_membership`,
keyword args `requested_by_id=None`, `mode="incremental"|"full"`.
Проверяются только организации текущего канонического справочника. Инкрементальный
запуск выбирает новые организации и те, у которых изменились name/full_name,
ИНН/ОГРН/ОКПО. Изменение определяется fingerprint; у модели Organization нет
универсального `updated_at`. Полный запуск проверяет весь справочник.
Lookup использует точный ОГРН, при его отсутствии/неподходящем формате — ИНН
с проверкой нормализованного имени. Если нет пригодного идентификатора, запуск
отклоняется с `sro_candidate_identifier_missing`, без удаления прежних записей.
Ответ дополнительно сопоставляется по ИНН+ОГРН через внутренний индекс. Отсутствие
ОКПО, конфликт идентификаторов, неоднозначная организация, неизвестный статус,
неполные обязательные поля или missing/unsafe SRO URL отправляют строку в quarantine.
Новые организации из внешнего ответа не создаются.
Одна запись соответствует паре canonical organization UID + SRO ID. Record UID
стабилен между загрузками, включая переход `active``excluded`. SRO ID/URL
берётся из same-site ссылки или точного ID, разрешённого по same-site sitemap;
по одному похожему названию URL не угадывается. Для даты допуска читается same-site
страница members по ИНН. Неизвестная дата остаётся `null` с причиной
`not_found`, `sro_page_unresolved` или `parse_error`. Наблюдаемая дата реестра
не подменяется датой загрузки.
Raw ответы сохраняются в приватном ZIP artifact с manifest URL→файл; отдельные
staged rows содержат source fields, нормализованный payload и причину quarantine.
Для SRO используется отдельный `FileSystemStorage`: `PARSER_PRIVATE_ARTIFACT_ROOT`
по умолчанию равен `<repository-root>/private-parser-artifacts`, вне `MEDIA_ROOT`.
Путь обязан быть абсолютным и не находиться внутри `MEDIA_ROOT`, в том числе после
разрешения symlink. Ошибка конфигурации запрещает файловую операцию. Приватный storage
не выдаёт публичный URL; файлы создаются с правами `0600`, каталоги `0700`.
Остальные источники сохраняют прежний `default_storage`.
В контейнерном окружении задайте `PARSER_PRIVATE_ARTIFACT_ROOT=/app/private-parser-artifacts`
и подключите к web/worker один постоянный volume по этому пути. Каталог должен быть
доступен пользователю приложения для записи и не должен монтироваться в public media,
static или reverse-proxy document root. Существующая retention-команда удаляет private
файлы через тот же `artifact.file.delete()`; удаление/пересоздание контейнера не должно
удалять volume. Миграция `0037` меняет storage в состоянии модели без перемещения данных.
До этой версии живой SRO сбор не запускался. Если оператор ранее создал SRO artifacts
в старом MEDIA_ROOT вручную, их требуется отдельно перенести в private root с сохранением
относительных имён и убрать публичные копии до открытия источника; автоматического
fallback на public raw нет.
Публикация записей, checkpoint организаций, load log и terminal job выполняется
в одной транзакции после завершения сбора всех кандидатов. Transport/schema failure,
нарушение целостности или отменённая job откатывает публикацию и checkpoints целиком.
Незавершённый batch остаётся диагностическим и не становится current snapshot.
Завершённый сбор может закончиться `success` с quarantine, как в примере истории
исходного контракта. Валидные строки публикуются, включая валидные членства организации,
у которой другая строка оказалась некорректной. Неизвестный статус, отсутствующий
SRO ID или URL, конфликт идентификаторов не исправляются догадками: исходная строка
остаётся в quarantine с причиной. У таких организаций прежние членства, не обновлённые
валидной строкой этого batch, сохраняются с прежними UID, payload и load batch.
Ошибка в строке не доказывает прекращение других членств. Для полностью проверенных
организаций исчезнувшие членства удаляются; допустимый пустой ответ удаляет их все.
При incremental неизменённые, не опрошенные организации также сохраняются.
Checkpoint полностью проверенной организации обновляется. У организации с quarantine
прежний checkpoint удаляется в той же транзакции, чтобы следующий incremental повторил
lookup даже при неизменившемся fingerprint. Raw artifact и staged audit сохраняются.
Если нет ни одной валидной строки и ни одного подтверждённого пустого lookup `200`,
quarantine отклоняет запуск с `sro_incomplete_membership_scan`; смесь invalid rows
и неоднозначных `404` не считается подтверждённым отсутствием. Для полностью состоящего
из `404` запуска сохраняется отдельный `sro_ambiguous_empty_scan`. В обоих случаях
прежние записи и checkpoints остаются без изменений.
Если incremental не нашёл изменившихся организаций, batch имеет нулевую дельту
и наследует source registry date/version последней успешной публикации; ссылка
на неё сохраняется в `metadata.base_snapshot_artifact_id`. Данные не переписываются.
После commit безопасно ставится существующая задача обновления общих export
artifacts. Ошибка брокера после commit не превращает успешную публикацию в failure;
готовность выгрузки отражает собственный контракт export service.
## Счётчики и диагностика
`artifact.published_count`, `result.published_records_count` и `load_log.records_count`
содержат размер текущего опубликованного набора источника. Для incremental это
общий результат, включая неизменённые организации. `metadata.snapshot_records_count`
и `snapshot_organizations_count` дают такие же общие record/distinct organization
counts; `batch_published_records_count` и `updated_records_count` — число опубликованных
строк текущей дельты. При full с quarantine общий count также включает сохранённые
старые членства. `retained_records_count` — число старых строк организаций с quarantine,
которые не были обновлены валидным upsert и сохранены от удаления;
`quarantined_organizations_count` — число организаций хотя бы с одной строкой quarantine;
`completed_organizations_count` — число полностью проверенных организаций без quarantine,
включая допустимые пустые результаты. Эти три поля доступны в metadata API.
В приватном artifact `confirmed_empty_lookup_200_count` отдельно фиксирует подтверждённые
пустые `200` для проверки допустимости публикации. `parsed_count`, quarantine и
candidate/queried counts относятся к текущей проверке; складывать их с общим published
count нельзя. Новая дата публикации не изменяет прежнюю lineage сохранённых записей.
Источник публикует весь собственный справочник; карточка/list/dashboard для трёх
новых источников используют весь published scope. Coverage ОПК показывается
отдельно. МСП и бюджет продолжают полную замену: новый scoped аргумент публикации
по умолчанию `None` сохраняет их поведение.
Для расследования используют job status/error, artifact status,
`metadata.rejection_reason`, `rejection_reasons` и staged disposition/reason.
Они содержат ограниченные коды ошибок, не raw HTML. Partial raw ZIP сохраняется
при ошибке уже начатого сбора; предыдущая публикация остаётся доступна.
Приватные raw artifacts удерживаются минимум 90 дней и минимум 10 последних
успешных SRO batches; неуспешные batches не вытесняют эти десять успешных.
## Расписания и граница проверки
Миграция `parsers.0036_sro_disabled_schedules` создаёт **выключенные** schedules:
daily incremental в 04:00 Europe/Moscow; monthly full 16-го числа в 04:00
Europe/Moscow — после наблюдаемого обновления реестра 15-го. Перед включением
нужно подтвердить фактическую периодичность/условия разрешения. Расписание само
не открывает gate; оба механизма по умолчанию выключены.
На 14.09.2026 query/search и живой SRO snapshot не запускались. Основная страница
без query доступна через read-only web; свежий robots.txt получить не удалось.
Указанные в source-first спецификации `Crawl-delay: 3` и query/search disallow
учтены консервативно. DOM selector/mapping проверены синтетическими fixtures формы
из `docs/source-integration/sources/sro-membership-check/backend-api.md`, а не
полным live scan. После разрешения владельца обязательна ограниченная проверка
фактической разметки перед первой полной загрузкой.