Перейти к содержанию

Источники: мок-сервис и обогащение

Здесь две части: мок-сервис источников (T06) и обогащение заявки — адаптеры и оркестратор (T07, раздел «Обогащение заявки»).

Мок-сервис

Играет все источники SPEC §5 по данным синтетического мира (apps/world/): внутренние счета и карточку клиента, ЕГРЮЛ / ЕГРИП, ФНС (+ГИР БО), БКИ, ФССП, арбитраж и Федресурс, 115-ФЗ. # MOCK: никаких обращений к настоящим системам нет.

Сервис только изображает источник: отдаёт ответ с задержкой или падает так, как падает настоящая интеграция. Конверт SourceResponse (статус, число попыток, задержка, ошибка), таймауты и повторы — работа оркестратора (T07).

Как пользоваться

from apps.sources import service_from_env

service = service_from_env()  # мир и настройки из переменных окружения
payload = await service.fetch('bki', '5406000001')  # BkiPayload

fetch(source, inn) — асинхронный, поэтому источники можно опрашивать параллельно (asyncio.gather). Он возвращает payload своего контракта (AccountsPayload, EgrulPayload, …) или бросает исключение:

Исключение Когда Что делает сервис
SourceUnavailable источник ответил ошибкой ждёт обычную задержку и падает
SourceTimeout источник завис ждёт hang_seconds (15 с — дольше таймаута адаптера 10 с, SPEC §6) и падает. Адаптер должен сам оборвать ожидание, например asyncio.wait_for
SourceNotFound в мире нет компании с таким ИНН ждёт обычную задержку и падает

Для разовых выборок без задержек (например, подбор персон в T12) ответ источника можно взять напрямую: payload(company, source).

HTTP

apps/sources/router.py — тот же сервис по HTTP: GET /mock-sources/{source}/{inn}. Ответы: 200 — payload, 404 — неизвестный ИНН или источник, который сервис не играет, 503 — ошибка, 504 — таймаут (после зависания). Роутер пока никуда не подключён (см. «Что нужно следующим задачам»):

from apps.sources import service_from_env
from apps.sources.router import make_router

app.include_router(make_router(service_from_env))  # мир строится при первом запросе

Откуда берётся мир

load_world() читает мир из WORLD_DIR (по умолчанию apps/world/out), если его записала текущая версия генератора. Иначе — или если папки нет — строит мир заново по WORLD_SEED, WORLD_N и WORLD_AS_OF (по умолчанию 42, 5000 и 2026-10-01). Мир по умолчанию строится около 5 секунд.

Настройки

Все переменные необязательные. Значения по умолчанию подходят для демо: задержки реалистичные, случайных сбоев нет.

Переменная По умолчанию Что задаёт
MOCK_SOURCES_LATENCY_SCALE 1 Множитель задержек. 0 — ответ сразу (тесты), 0.1 — быстрое демо
MOCK_SOURCES_FAILURES off on — случайные сбои с частотами из таблицы ниже
MOCK_SOURCES_HANG_SECONDS 15 Сколько висит источник при таймауте. Множитель задержек на него не действует
MOCK_SOURCES_SCENARIOS — Принудительные исходы по ИНН, JSON: {"<ИНН>": {"<источник>": "<сценарий>"}}
WORLD_DIR, WORLD_SEED, WORLD_N, WORLD_AS_OF см. выше Какой мир играть

Задержки и частоты сбоев по источникам (DEFAULT_BEHAVIOUR в service.py). # MOCK: задержки взяты в диапазоне примеров из tests/fixtures/sources/, частоты сбоев — оценка.

Источник Медиана задержки Ошибка Таймаут
Счета (accounts) 140 мс — —
Карточка клиента (client_card) 60 мс — —
ЕГРЮЛ / ЕГРИП (egrul) 800 мс 2% 1%
ФНС (fns) 1,8 с 3% 2%
БКИ (bki) 2,4 с 2% 2%
ФССП (fssp) 1,1 с 5% 5%
Арбитраж и Федресурс (arbitration) 1,3 с 3% 3%
115-ФЗ (aml) 400 мс 1% 1%

Задержка — логнормальная вокруг медианы (σ = 0,35). Внутренние источники не бывают недоступны (SPEC §5), поэтому случайные сбои их не касаются.

Сценарии

Сценарий задаёт исход для одного ИНН и одного источника и работает всегда, даже при MOCK_SOURCES_FAILURES=off:

  • timeout / error — каждый вызов;
  • timeout:N / error:N — первые N вызовов, потом источник отвечает. Так проверяются повторы адаптера: error:1 — первая попытка падает, вторая проходит;
  • ok — источник всегда отвечает, даже при включённых случайных сбоях.

Пример: ФССП не отвечает, а ЕГРЮЛ отвечает только со второй попытки:

MOCK_SOURCES_SCENARIOS='{"5406000001": {"fssp": "timeout", "egrul": "error:1"}}'

Воспроизводимость

Каждый вызов разыгрывается из своего потока случайных чисел: зерно мира, ИНН, источник и номер вызова для этой пары. Поэтому одна и та же последовательность вызовов даёт одни и те же задержки и сбои при каждом запуске. Повторная попытка — это следующий номер вызова, поэтому после сбоя она может пройти. reset() обнуляет счётчики вызовов.

Случайные сбои по умолчанию выключены. Сбой ФНС или ФССП меняет решение (флаг и штраф к оценке, SPEC §5), а одинаковые входы должны давать одинаковый результат. Сбои для демо задаются сценариями.

Обогащение заявки (T07)

enrichment.py, facts.py, client.py. Этап 3 конвейера (SPEC §6): по заявке опросить все источники параллельно, собрать ответы и факты и решить, можно ли идти дальше.

from apps.sources import RetryPolicy, enrich, service_from_env

result = await enrich(request, service_from_env(), policy=RetryPolicy.from_env())
result.responses             # 8 × SourceResponse, в порядке SOURCES
result.facts                 # list[Fact]
result.complete              # все обязательные источники ответили
result.unavailable_required  # → андеррайтер, «источник недоступен» (SPEC §11)
result.degraded              # необязательные источники, которые не ответили → флаг и штраф (T14)
result.company_not_found     # ЕГРЮЛ / ЕГРИП не знает такого ИНН

Адаптеры

Оркестратору нужен SourceClient — объект с одним методом fetch(source, inn): он возвращает payload источника или бросает SourceUnavailable, SourceTimeout, SourceNotFound. Это и есть место, где мок заменяется настоящей интеграцией.

  • MockSourceService (T06) — в том же процессе, по умолчанию.
  • HttpSourceClient(base_url) — тот же сервис по HTTP через router.py, как настоящая интеграция. Ответ проверяется по контракту источника. Сетевая ошибка и ответ 5xx — SourceUnavailable, 504 — SourceTimeout, 404 — SourceNotFound.

Таймауты и повторы

По умолчанию Переменная
Таймаут одной попытки 10 с (SPEC §6) SOURCE_TIMEOUT_SECONDS
Попыток всего, считая первую 3 SOURCE_ATTEMPTS
Пауза перед попыткой k 0,5 с × (k − 1) SOURCE_RETRY_BACKOFF_SECONDS
  • Повторяются таймаут и ошибка. «Компания не найдена» не повторяется: повтор её не найдёт.
  • Любое другое исключение клиента считается ошибкой источника: сломанная интеграция не роняет конвейер.
  • latency_ms — всё время на источник, со всеми попытками и паузами; received_at = requested_at + latency_ms, только при успехе.
  • Тексты ошибок — для досье: «Источник не ответил за 10 с, 3 попытки», «Источник вернул ошибку (…), 2 попытки», «Компания с ИНН … не найдена».
  • Худший случай по умолчанию — 31,5 с на источник (три таймаута и паузы). Источники опрашиваются параллельно, поэтому это и время всего этапа. Это чуть больше цели SPEC §6 (p95 до 30 с), но цель про 95% заявок: она выполняется, пока источник, зависающий три раза подряд, встречается реже чем в 5% заявок.

Политика отказов (SPEC §5, §11)

Источник Если не ответил после всех попыток
Счета, карточка клиента Обязательный. Случайно не падают (SPEC §5), но если интеграция сломана — в unavailable_required
ЕГРЮЛ / ЕГРИП, БКИ, 115-ФЗ Обязательный → unavailable_required: заявка уходит андеррайтеру с причиной «источник недоступен»
ФНС, ФССП, арбитраж и Федресурс Необязательный → degraded: конвейер идёт дальше, флаг и штраф к оценке добавляет T14

Оркестратор только сортирует отказы. Куда перевести заявку, решает рабочий процесс (T19), маршрут — T18.

Согласия на запрос в БКИ (218-ФЗ)

БКИ опрашивается только с согласием клиента (ApplicationRequest.consents). У ООО нужны два: bki_company с ИНН самой компании и bki_owner собственника. У ИП — одно, bki_owner: предприниматель и есть бизнес.

  • Нет нужного согласия — БКИ не опрашивается вовсе. Ответ — ошибка «Нет согласия на запрос кредитной истории (218-ФЗ): компания» (или «собственник») с attempts = 1, источник попадает в unavailable_required.
  • В ответе остаются только отчёты тех, кто дал согласие. Если отчёт собственника пришёл, а согласие дал другой человек, ответ — ошибка «Согласие … дано не тем лицом: собственник».

События для экрана «Анализируем ваш бизнес» (T27)

enrich(..., on_event=handler) сообщает о каждом шаге: SourceEvent(source, attempt, status, final, error). Статусы: started — попытка началась; ok, timeout, error, not_found, no_consent — попытка закончилась. final отмечает последнее событие источника. Обработчик может быть обычной функцией или корутиной.

Факты

facts.py. Один Fact на пару «название — источник» (SPEC §2, принцип 2). Надёжность: bank у счетов и карточки клиента, registry у госисточников и БКИ, declared — если есть только слова клиента. fetched_at — время ответа источника.

Факт Источник Заявлено клиентом Что это
legal_form ЕГРЮЛ да ip / ooo
registry_status ЕГРЮЛ active, liquidating, …
business_age_months ЕГРЮЛ Полных месяцев с регистрации до дня ответа
okved_main ЕГРЮЛ Основной ОКВЭД
director_changed_6m ЕГРЮЛ Директор сменился за 6 месяцев (директор при регистрации — не смена)
founder_changed_6m ЕГРЮЛ То же для учредителей, только ООО
annual_revenue ФНС да Выручка из ГИР БО (ООО) или доход по декларации (ИП) за последний год, как сдана
employees ФНС Численность
tax_debt, accounts_suspended, mass_address, director_disqualified ФНС Как в ответе ФНС
operating_inflows_12m Счета Поступления от покупателей за последние 12 месяцев
monthly_debt_payments БКИ Платежи по действующим продуктам бизнеса: отчёт компании у ООО, единственный отчёт у ИП
internal_stop_list Карточка клиента Внутренний стоп-лист
fssp_debt_remaining ФССП Остаток долга по всем исполнительным производствам
claims_as_defendant Арбитраж Сумма исков к компании, кроме отклонённых
bankruptcy_notice Федресурс Есть сообщение о банкротстве
aml_list_match, transit_operations, zsk_zone 115-ФЗ Как в ответе 115-ФЗ
loan_purpose — да Цель кредита: только заявлена, источника нет

Факты — значения источников как есть. Всё, что считается по многим значениям (правила просрочек, признаки скоринга, показатели качества данных), — библиотека признаков (T08, T09); пороги — стоп-факторы и флаги (T14). Например, выручка ФНС в факте — за последний сданный год как есть, даже если компания работала только часть года; пересчёт на полный год и на последние 12 месяцев — в библиотеке признаков (apps/api/features/revenue.py). Если источник не ответил, его факты остаются только с заявленным значением, а факты, которых клиент не заявлял, пропадают. Тест test_facts_match_the_t00_example проверяет, что из примеров источников в tests/fixtures/ получаются подтверждённые значения из facts.json.

Что нужно следующим задачам

  • T13 (хранилище фактов): Enrichment сериализуется в JSON и обратно (model_dump_json / model_validate_json). Факты — готовые Fact из контракта.
  • T14 (стоп-факторы и флаги): пороги не здесь. degraded — список необязательных источников, за которые нужен флаг «источник недоступен» и штраф. annual_revenue из ФНС — за последний сданный год как есть, у компании, зарегистрированной в прошлом году, это неполный год (см. «Известные ограничения» в apps/world/README.md).
  • T19 (статусы): company_not_found → заявку нельзя рассматривать; not complete → очередь андеррайтера с причиной «источник недоступен»; иначе — стоп-факторы и скоринг.
  • T25 (согласия): без согласий на БКИ заявка дальше андеррайтера не уйдёт — клиентский интерфейс должен собирать bki_company с ИНН компании и bki_owner с ИНН основного собственника (у ИП — с ИНН предпринимателя).
  • T27 (экран статусов): on_event, см. выше. HTTP-роутер мок-сервиса по-прежнему не подключён к apps/api/main.py: по умолчанию оркестратор зовёт сервис в процессе.
  • T12 (персоны): сценарий для персоны (например, ФССП не отвечает) задаётся в MOCK_SOURCES_SCENARIOS, мир при этом не меняется.

Упрощения

В коде помечены # MOCK: и # SIMPLIFICATION:.

Упрощение Как было бы в реальности
Все источники отвечают из одного синтетического мира, по ИНН компании Каждая интеграция — свой протокол (СМЭВ, API БКИ, выгрузки ФССП и картотеки арбитражных дел), свои форматы и авторизация
Ответы урезаны до полей контракта (SPEC §5) Настоящие ответы ЕГРЮЛ, ФНС и БКИ намного шире
Задержка не зависит от нагрузки и времени суток; частоты сбоев — оценка Задержки и доступность по SLA и мониторингу каждой интеграции; сбои идут сериями
Ошибка источника одна на всех — SourceUnavailable Разные коды ошибок: нет согласия, превышен лимит запросов, неверный запрос, техническая ошибка
Запрос в БКИ не оставляет записи о запросе нашего банка в отчёте Каждый запрос попадает в кредитную историю субъекта
Ответы не кешируются и не тарифицируются Запросы в БКИ платные, их кешируют на срок действия согласия
Согласие на БКИ проверяется по виду и ИНН, без срока действия и подписи Согласие подписано ПЭП, хранится и проверяется по сроку (218-ФЗ)
Пауза между попытками растёт линейно, без случайного разброса; нет «предохранителя», который перестаёт звать упавший источник Экспоненциальная пауза с разбросом и circuit breaker на каждую интеграцию

Настройки организатора

Страница /organizers задаёт глобальный режим ответа и задержку каждого из восьми источников. Онлайн-конвейер подключает к мок-сервису провайдер общих настроек из БД (apps/api/organizers.py): каждый запрос, включая повторную попытку, получает актуальные значения. Принудительные ok, error и timeout имеют приоритет над сценариями конкретного ИНН и случайными сбоями; default сохраняет базовое поведение. При timeout задержка не применяется. Ночной пакет предодобрения использует базовые настройки. Подробности входа и API — в разделе «Панель организатора» корневого README.