Источники: мок-сервис и обогащение¶
Здесь две части: мок-сервис источников (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.