Жизненный цикл заявки v2 (T19)¶
workflow.py ведёт заявку по статусам SPEC §13, пишет журнал действий и запускает автоматический расчёт: обогащение → качество данных → стоп-факторы → скоринг → предложения → маршрут → Decision. Заявка v2 хранится в таблице loan_applications, журнал — в application_events, факты — в хранилище фактов (fact_store.py, T13). Код v1 удалён; исторические таблицы сохранены, но в работе конвейера не участвуют (см. MIGRATIONS.md).
Статусы и переходы¶
submitted -> enriching -> scoring -> offered | declined | in_review
scoring -> needs_data (данные THIN, выписок ещё не просили, T23)
enriching -> declined (компания не найдена в ЕГРЮЛ / ЕГРИП)
enriching -> in_review (обязательный источник недоступен)
needs_data -> enriching (клиент загрузил выписки или продолжил без них, T23, STATEMENTS.md)
in_review -> offered | declined | needs_data (сотрудник, T20, STAFF_ACTIONS.md)
in_review -> submitted (сотрудник перезапускает расчёт, T20)
offered -> accepted -> documents -> signed -> conditions -> disbursed (T21, DISBURSEMENT.md)
offered -> expired (срок предложения истёк)
до подписания из любого статуса -> withdrawn (клиент)
enriching | scoring -> submitted (прерванный расчёт возвращается в очередь)
Финальные статусы: declined, disbursed, withdrawn, expired. Таблица переходов — TRANSITIONS. Статус меняет только transition(db, app, to, actor=..., message=...): переход вне таблицы — ошибка IllegalTransition, каждый переход пишет событие в журнал. Переходы enriching → declined / in_review, enriching | scoring → submitted и in_review → submitted в схеме SPEC §13 не нарисованы: это ранний выход из конвейера, повтор после сбоя и повторный расчёт по решению сотрудника (например, когда обязательный источник снова доступен).
Журнал действий¶
application_events — только добавление, одна строка на событие:
| Поле | Что |
|---|---|
at |
Время (UTC, ISO 8601) |
actor |
system, client:<id пользователя> или staff:<id пользователя> |
kind |
status — смена статуса; source — шаг вызова источника; action — данные собраны; decision — решение; error — сбой расчёта; review, proposal, signature — действия сотрудника и подписи комитета (STAFF_ACTIONS.md). Код подписания, неверный код и проверка отлагательных условий (T21) — события action |
from_status, to_status |
Для status |
message |
Текст для человека |
data |
Подробности: попытка и ошибка источника, маршрут, грейд, PD, версии модели и политики, id предложений, коды стоп-факторов и флагов |
События source пишутся сразу, по ходу обогащения (enrich(..., on_event=...)), поэтому экран «Анализируем ваш бизнес» (T27) может опрашивать GET /api/v2/applications/{id} и показывать статус каждого источника (поле sources).
Автоматический расчёт¶
Worker (worker.py, раз в 0,5 с) вызывает workflow.tick() и затем disbursement.tick() — отлагательные условия и выдачу (T21, DISBURSEMENT.md). workflow.tick():
recover_stale()— заявки, которые зависли вenriching/scoringдольше аренды (120 с), считаются сбоем;expire_offers()—offered→expired, когда прошёл срок последнего предложения (Offer.valid_until, 10 дней);run_next()— берёт самую старую заявкуsubmitted. Захват — это сам переходsubmitted → enrichingс проверкой версии строки, поэтому два worker не возьмут одну заявку. Если таких нет — заявку, которая вернулась изneeds_data(claim_resumed): она уже вenriching, но без аренды, и захват — это взятие аренды.
После этого worker вызывает preapproval.tick(): раз в сутки, в отдельном потоке, считается предодобренный овердрафт (T22, PREAPPROVAL.md).
run_application:
enrich— все источники параллельно (T07). Заявка, вернувшаяся изneeds_data, источники заново не опрашивает, если в прошлый раз ответили все обязательные: берутся сохранённые ответы. Иначе каждая выписка стоила бы клиенту лишнего запроса в БКИ, а запросы БКИ снижают балл.- Одна транзакция: факты в хранилище фактов (с фактами из выписок клиента,
statement_upload), ответы источников (без фактов) вloan_applications.enrichment,DataQuality(T09; с выписками — по нашему счёту вместе с ними) вdata_quality. Если обогащение полное — переход вscoring. decide(...)— чистая функция: одинаковые входы дают одинаковое решение.- Вторая транзакция:
Decisionвloan_applications.decision, событиеdecision, переход в статус решения.
Если клиент отозвал заявку во время расчёта, результат отбрасывается. Если расчёт упал, заявка возвращается в submitted с задержкой 5 × номер попытки секунд; после третьей попытки уходит в in_review без решения (событие error).
Как принимается решение (decide)¶
| Ситуация | Статус | Маршрут, причины |
|---|---|---|
| Компания не найдена в ЕГРЮЛ / ЕГРИП | declined |
auto; причина company_not_found, дата повторного обращения не задаётся |
| Обязательный источник недоступен | in_review |
underwriter, route_source_unavailable; скоринга и предложений нет |
| Стоп-фактор (T14) | declined |
auto, route_stop_factor; скоринга нет |
Карта не подходит к признакам (FeatureSetMismatch, T15) |
in_review |
underwriter, route_score_unavailable; предложений нет |
Данные THIN, клиент ещё не отвечал на запрос данных (statements is None) |
needs_data |
маршрут и причины считаются как обычно (route_data_thin), балл есть; предложений нет; requested_documents — выписка из другого банка за 12 месяцев (T23) |
Маршрут auto, есть предложения |
offered |
route_auto |
Маршрут auto, предложений нет |
declined |
причины из генератора предложений (T17), повторное обращение через 90 дней |
| Маршрут андеррайтер или комитет | in_review |
причины T18; посчитанные предложения остаются в Decision, сотрудник одобряет или меняет их (T20, STAFF_ACTIONS.md) |
Порядок вызовов: data_quality_for (наш счёт плюс выписки клиента) → evaluate_stop_factors → features_from_responses → score (основная модель решает, претендент только записывается) → check_statements (флаги по выпискам) → cash_flow (наш счёт плюс принятые выписки) → generate_offers(..., route_for(grade, flags, data_quality, statements=...), день решения) → route_application. Стоп-факторы, признаки и балл считаются только по нашим данным и госисточникам, выписки на них не влияют (STATEMENTS.md).
Маршрут заявки при нескольких предложениях. Предложения одной заявки могут считаться на разных маршрутах (1 млн ₽ — андеррайтер, 730 тыс. ₽ — автоматически). Заявка идёт по самому строгому из них, а причины маршрута считаются для суммы этого предложения. Так одно рассмотрение сотрудником покрывает все варианты, которые увидит клиент. Без предложений маршрут считается для запрошенной суммы.
Дата расчёта признаков. Признаки и стоп-факторы считаются на дату синтетического мира (WORLD_AS_OF, по умолчанию 2026-10-01), а не на сегодня: мир — снимок, и с каждым днём возраст бизнеса и окна запросов БКИ уезжали бы от данных. Срок действия предложений и дата повторного обращения считаются от реального дня решения (МСК).
Версия политики. Decision.policy_version — одна строка, а политик три. Пишем их все: credit=credit_policy_v0;data_quality=data_quality_v0;features=features_v0. Предложение отдельного поля в контракте — в docs/v2/NOTES.md, Unassigned.
API (/api/v2)¶
| Метод | Кто | Что |
|---|---|---|
GET /api/v2/companies/{inn} |
клиент | Поиск в мок-ЕГРЮЛ/ЕГРИП для карточки T25. Возвращает сведения компании и ИНН основного собственника; 404 — компания не найдена, 503 — источник недоступен. Нужен вход в кабинет |
POST /api/v2/applications |
клиент | {"idempotency_key", "request"}: request — ApplicationRequest без application_id и submitted_at (их задаёт сервер). Повтор с тем же ключом возвращает ту же заявку. Инвестиционный кредит без оборудования — 422 |
GET /api/v2/applications[?status=] |
клиент — свои, сотрудник — все | Список |
GET /api/v2/applications/{id} |
клиент, сотрудник | Заявка |
GET /api/v2/applications/{id}/events |
сотрудник | Полный журнал |
POST /api/v2/applications/{id}/accept |
клиент | {"offer_id", "revision"}: offered → accepted → documents (документы формируются сразу, T21). Просроченное предложение переводит заявку в expired и отвечает 409 |
POST .../sign/code, .../sign, GET .../documents/{doc_id} |
клиент | Подписание кодом и документы (T21, DISBURSEMENT.md) |
POST /api/v2/applications/{id}/withdraw |
клиент | {"revision"}: → withdrawn |
POST /api/v2/applications/{id}/statements |
клиент | multipart/form-data: files (до 5 файлов по 2 МБ) и revision: needs_data → enriching. Нераспознанный файл, чужой ИНН, повтор файла — 422 с текстом для клиента (T23, STATEMENTS.md) |
POST /api/v2/applications/{id}/statements/skip |
клиент | {"revision"}: продолжить без выписки, needs_data → enriching |
POST /api/v2/applications/{id}/review, .../review/sign |
сотрудник | Действие с кодом причины и комментарием; вторая подпись комитета (T20, STAFF_ACTIONS.md) |
GET /api/v2/staff/reason-codes, GET /api/v2/staff/overrides |
сотрудник | Коды причин; как часто сотрудники не соглашаются с системой |
GET /api/v2/preapproval?inn=…, POST /api/v2/staff/preapproval/run |
клиент, сотрудник | Предодобренный овердрафт для баннера; запуск расчёта (T22, PREAPPROVAL.md) |
POST /api/assistant/messages |
клиент | { "message", "application_id"? }: если передан ID открытой v2-заявки, API проверяет владельца и передаёт помощнику только клиентское представление этой заявки; без ID — общий справочный контекст. Никаких действий над заявкой (T37). |
revision — версия строки заявки. Если она устарела, ответ 409: заявка изменилась.
Клиент видит запрос, смены статусов, ход опроса источников (sources: источник, попытка, статус, без текста ошибки), свои выписки (statements: файл, банк, период, время загрузки) и сокращённое решение (client_decision): клиентские тексты причин без повторов, предложения без ожидаемой прибыли, контрфактические подсказки, уровень данных с фразой для клиента, дату повторного обращения. Флаги, баллы, PD, модель-претендент, тексты для сотрудника и журнал с исполнителями клиенту не отдаются: среди них есть признаки ПОД/ФТ (SPEC §1.11). Сотрудник видит всё: Decision, DataQuality, статусы источников, факты, полный журнал, pending_review — решение, которое ждёт второй подписи комитета, — и statements — выписки клиента целиком (null — клиент ещё не отвечал на запрос данных, [] — продолжил без выписки).
Упрощения¶
# MOCK:GET /api/v2/companies/{inn}возвращает запись синтетического мира из мока ЕГРЮЛ/ЕГРИП. Галочки T25 и время согласия моделируют сбор согласий в демо: подтверждение личности, юридически значимое подписание и постоянный реестр согласий не реализованы.# MOCK:признаки считаются на дату синтетического мира, а не на дату ответа источников (см. выше). С реальными источниками дата —snapshot_date(responses).# SIMPLIFICATION:очередь — это колонкаstatus, которую опрашивает worker внутри процесса API. В банке это брокер сообщений или workflow-движок с собственными повторами и очередью ошибок.- Срок предложения истекает при очередном проходе worker или при попытке выбрать предложение, а не точно в полночь.