Статусная машина бронирования
Версия: 1.0 Дата: 26.04.2026 Статус: Готов к обсуждению
Назначение документа
Документ определяет полную статусную машину каноничной сущности Booking платформы Vitiana — все состояния, все допустимые переходы, инварианты на каждое состояние, правила идемпотентности, обработку неизвестного внешнего состояния (unknown external state), восстановление после сбоя, terminal states, и интеграцию с платёжным контуром, контуром поставщиков и контуром взаиморасчётов.
Включает:
- Полный список 14 каноничных состояний (расширение тезисного списка из API Contracts).
- Граф допустимых переходов с триггерами и инвариантами.
- Правила идемпотентности для каждой операции.
- Различение состояний
supplier_confirmedиplatform_confirmed(зафиксировано в Семантика предложений, цены, бронирования, детализируется здесь). - Обработку состояния
unknown_external_state— ключевая операционная категория для устойчивости. - Replay-safe механизмы — восстановление состояния через журнал событий.
- Терминальные состояния (terminal states) и пост-обработку.
- Связь со state machine
PaymentIntent(см. Платёжный домен). - Связь с сагой пакетного тура (см. Операционная модель конструктора туров).
- Каноничные таймауты и правила перехода по таймауту.
- Запрещённые переходы и защита от них.
- Метрики и наблюдаемость.
- Фазы развёртывания.
Документ читается после:
- API Contracts → раздел «Базовые Доменные Состояния Booking Surface» — тезисный список, который этот документ детализирует.
- Семантика предложений, цены, бронирования → различение
supplier_confirmedvsplatform_confirmed. - Business Services § 4 → Booking Service с минимальным набором состояний.
- Платёжный домен → стыковка с состояниями
PaymentIntent. - Операционная модель конструктора туров →
TourBookingTransactionсага. - Жизненный цикл пост-бронирования → пост-обработка после
confirmed. - Партнёрские взаиморасчёты → settlement после
confirmed.
В корневых документах зафиксированы тезисные списки состояний в разных контекстах. Этот документ синтезирует их в единую state machine с явными переходами, инвариантами и правилами устойчивости.
Бронирование — самый критичный финансовый контур платформы. Ошибка в state machine — это либо двойное списание клиента, либо потерянное бронирование, либо рассогласование с поставщиком, либо невыплата поставщику. Всё это — реальные деньги и реальная ответственность.
Реальное состояние реализации (home-to-go-api): рабочий Stuba booking module существует, но без полноценной state machine. Текущее ведение состояний — линейное и упрощённое. Целевое состояние — то, что определяется этим документом.
Правило 00000 (платформа главенствует над поставщиками) применяется здесь напрямую: каноничная state machine Booking — модель платформы, не отражение состояний поставщиков. Поставщик возвращает свои собственные коды состояний — слой приёма данных переводит их в каноничные.
Главное решение
Booking — каноничная state machine платформы с 14 состояниями, явными переходами, инвариантами, идемпотентностью и replay-safe журналом, не «черный ящик с упрощёнными состояниями».
Это означает:
- 14 каноничных состояний — каждое имеет точное определение и место в графе.
- Все переходы зарегистрированы через
BookingStateTransitionсущность с журналом для аудита. - Идемпотентность всех критических операций через
idempotency_key—create,confirm,cancel,amend,complete. - Различение
supplier_confirmedиplatform_confirmed— два разных момента; платформа фиксирует свои инварианты после ответа поставщика. unknown_external_state— first-class категория — когда поставщик не отвечает в разумное время или отвечает некорректно. Не «потерянное бронирование», а явное состояние с восстановлением.- Replay-safe — состояние можно восстановить из журнала событий после сбоя любого компонента.
- Каноничные таймауты для каждого ожидающего состояния — никаких бесконечных ожиданий.
- Terminal states — после
cancelled,failed,completedбронирование переходит в архивное состояние и не меняется. - Защита от запрещённых переходов — попытка перевести
cancelledобратно вconfirmedблокируется на уровне state machine.
Каноничные сущности (углубление существующих)
Уже зафиксированные сущности
В API Contracts и Семантика предложений, цены, бронирования уже зафиксирована сущность Booking с базовыми полями. Этот документ расширяет её операционными подсущностями.
Новые операционные сущности
BookingStateTransition — переход между состояниями
Сущность, представляющая факт перехода конкретного бронирования из одного состояния в другое.
{
transition_id: UUID,
booking_id: UUID,
from_state: enum,
to_state: enum,
trigger: enum,
triggered_by: object,
idempotency_key: string,
preconditions_satisfied: array,
invariants_validated: array,
caused_by_event_id: UUID,
caused_by_supplier_response: object,
saga_step_id: UUID,
applied_at: timestamp
}
Поля:
trigger— что инициировало переход (user_action/partner_api_call/supplier_callback/timeout/restoration_job/saga_step/compensating_action/manual_override).triggered_by— детальный контекст триггера (кто, какая операция, какие параметры).idempotency_key— ключ, обеспечивающий идемпотентность: повтор той же операции с тем же ключом не создаёт второго перехода.preconditions_satisfied— какие предусловия были проверены и удовлетворены.invariants_validated— какие инварианты были сохранены при переходе.caused_by_event_id— если переход вызван доменным событием.caused_by_supplier_response— снимок ответа поставщика (для аудита).saga_step_id— связь с шагом саги пакетного тура (если применимо).
BookingInvariantCheck — проверка инварианта
Сущность, представляющая формальную проверку инварианта на бронировании.
{
check_id: UUID,
booking_id: UUID,
invariant_class: enum,
check_logic: object,
result: enum,
detected_at: timestamp,
resolved_at: timestamp,
resolution: enum,
severity: enum,
notes: string
}
Поля:
invariant_class— класс инварианта (payment_state_consistent/supplier_state_known/quote_not_expired/mor_assigned/tenant_active/compliance_satisfied).severity— серьёзность нарушения (error— блокирует переход;warning— допускает с пометкой;info— для журнала).
BookingRestorationJob — задача восстановления
Сущность, представляющая операцию восстановления бронирования из неопределённого состояния.
{
restoration_id: UUID,
booking_id: UUID,
restoration_class: enum,
triggered_at: timestamp,
completed_at: timestamp,
state_before_restoration: enum,
state_after_restoration: enum,
actions_taken: array,
supplier_query_results: array,
outcome: enum,
next_retry_at: timestamp,
retry_count: integer
}
Поля:
restoration_class— класс задачи (unknown_state_recovery— выяснение реального состояния у поставщика;timeout_triggered_check— плановая проверка по таймауту;manual_investigation— запрос ручного расследования;replay_from_event_log— пересборка состояния из журнала событий).outcome— итог (state_clarified/still_unknown_retry_scheduled/escalated_to_operator/terminal_resolution).retry_count— счётчик попыток (после максимума — эскалация).
Связь с другими каноничными сущностями
BookingStateTransition.idempotency_key— связан с ключом идемпотентности изPaymentIntent.idempotency_key(см. Платёжный домен).BookingStateTransition.saga_step_id— связь сTourBookingTransaction.saga_log(см. Операционная модель конструктора туров).BookingInvariantCheck— публикуется в Платформа данных и захват событий для аналитики.BookingRestorationJob— связана с операционным контуром из Операционная ось.
Полный список 14 состояний
draft
↓
submitted
↓
pending_revalidation
↓
pending_supplier_confirmation
↓
├──→ supplier_confirmed
│ ↓
│ platform_confirmed (≡ confirmed для внешних потребителей)
│ ↓
│ completed
│
├──→ partially_confirmed
│
├──→ unknown_external_state
│ ↓ (после восстановления)
│ возврат в один из других состояний
│
└──→ failed
(параллельный путь отмены — со многих состояний:)
↓
cancel_requested
↓
cancel_in_progress_supplier
↓
cancelled
(параллельный путь изменения — после confirmed:)
↓
amendment_in_progress
↓
├──→ confirmed (после успеха)
└──→ amendment_failed (откат к prior state)
Описание каждого состояния
1. draft — черновик
Бронирование создано, но ещё не отправлено на обработку. Минимально заполненная сущность с привязкой к Tenant, Quote (или его кандидатам), client_context.
Инварианты:
Tenantактивен.- Хотя бы один связанный
Quoteсуществует (опционально для черновиков, формирующихся итеративно).
Допустимые переходы:
- →
submitted(пользователь отправил черновик). - →
cancelled(пользователь удалил черновик).
2. submitted — отправлено
Бронирование отправлено в обработку. Платформа начинает выполнение pre-checks.
Инварианты:
Quoteдействителен (не истёк).Tenantактивен и не превысил лимит тарифа.- Платёжное намерение в состоянии
createdилиawaiting_method.
Допустимые переходы:
- →
pending_revalidation(требуется проверка актуальности предложения у поставщика). - →
pending_supplier_confirmation(если revalidation не нужна — пропуск). - →
failed(предусловия не выполнены). - →
cancel_requested(пользователь отменил до обработки).
3. pending_revalidation — ожидание повторной проверки
Платформа проверяет актуальность предложения у поставщика перед фиксацией. Это нужно для drift detection на момент бронирования (цена/доступность могла измениться с момента создания Quote).
Инварианты:
- Запрос к поставщику отправлен.
- Срок ожидания (timeout) активен.
Допустимые переходы:
- →
pending_supplier_confirmation(предложение актуально, переходим к фиксации). - →
failed(предложение устарело, повторное бронирование требует новогоQuote). - →
unknown_external_state(поставщик не ответил в срок). - →
cancel_requested(пользователь отменил во время ожидания).
4. pending_supplier_confirmation — ожидание подтверждения поставщика
Запрос на бронирование отправлен поставщику; платформа ждёт ответа. Это самое распространённое промежуточное состояние — типично длится секунды-минуты, для асинхронных поставщиков может длиться часы.
Инварианты:
PaymentIntentв состоянииauthorized(платёж зарезервирован).- Запрос к поставщику отправлен с уникальным
supplier_request_id. - Срок ожидания активен.
Допустимые переходы:
- →
supplier_confirmed(поставщик подтвердил). - →
partially_confirmed(поставщик подтвердил часть). - →
failed(поставщик отклонил). - →
unknown_external_state(поставщик не ответил в срок). - →
cancel_requested(отмена в момент ожидания — сложный случай, см. ниже).
5. supplier_confirmed — поставщик подтвердил
Поставщик дал положительный ответ. Бронирование у поставщика существует, но платформа ещё не зафиксировала свои внутренние инварианты (settlement, фиксация платежа, обновление аналитики).
Инварианты:
- Поставщик вернул
confirmation_idи положительное состояние. - Поле
supplier_confirmation_dataзаполнено.
Допустимые переходы:
- →
platform_confirmed(платформа завершила свои инварианты — единственный нормальный переход). - →
unknown_external_state(если последующие операции платформы провалились и состояние стало неопределённым).
Этот этап критичен: платформа уже должна клиенту услугу, но ещё не зафиксировала это во всех контурах. Запрещён прямой переход в failed или cancelled — отмена возможна только через стандартный поток отмены после platform_confirmed.
6. platform_confirmed (синоним confirmed для внешних потребителей) — платформа подтвердила
Все платформенные инварианты выполнены: платёж зафиксирован (PaymentIntent.captured), создан Settlement для взаиморасчёта с поставщиком, опубликовано доменное событие booking.confirmed, начата пост-обработка.
Инварианты:
PaymentIntent.state = captured.Settlementсоздан в состоянииpending.- Событие
booking.confirmedопубликовано (см. Первоначальная таксономия событий).
Допустимые переходы:
- →
completed(бронирование исполнено: дата выезда прошла). - →
cancel_requested(клиент или поставщик инициировал отмену). - →
amendment_in_progress(запрошено изменение — даты, состав, и т.д.).
Внешний вид: для всех внешних поверхностей и для партнёров это состояние называется просто confirmed. Внутреннее различение supplier_confirmed / platform_confirmed нужно только для саги и операционных команд.
7. partially_confirmed — частичное подтверждение
Поставщик подтвердил часть запрошенного (например, 2 из 3 номеров). Это состояние требующее решения — клиент должен явно согласиться на частичное или отменить.
Инварианты:
- Поставщик вернул частичное состояние.
- Платформа уведомила клиента и ожидает решения.
- Срок ожидания решения активен.
Допустимые переходы:
- →
platform_confirmed(клиент согласился на частичное; платежу делается частичная фиксация). - →
cancel_requested(клиент не согласен; полная отмена с возвратом). - →
unknown_external_state(если последующие операции провалились).
8. unknown_external_state — неопределённое внешнее состояние
Самое важное операционное состояние — поставщик не отвечает или ответил некорректно. Платформа не знает, существует ли бронирование у поставщика.
Это не «потерянное бронирование», а явное состояние с обязательным восстановлением.
Инварианты:
- Создан
BookingRestorationJobсо статусомunknown_state_recovery. - Срок повторной попытки настроен.
PaymentIntentостаётся вauthorized(не фиксируется до выяснения).
Допустимые переходы (после восстановления):
- →
pending_supplier_confirmation(можно повторить попытку безопасно). - →
supplier_confirmed(восстановление выяснило, что бронирование существует). - →
failed(восстановление выяснило, что бронирования нет). - →
cancel_requested(по решению оператора).
См. секцию «Обработка unknown_external_state» ниже.
9. failed — провал
Терминальное состояние неудачного бронирования. Бронирование не существует (ни у поставщика, ни на платформе с финансовыми обязательствами).
Инварианты:
PaymentIntent.state = voided(платёж снят без списания) или возврат в случае состоявшегося списания.- Событие
booking.failedопубликовано. - Причина провала зафиксирована в
failure_reason.
Допустимые переходы: нет (terminal state). Новое бронирование на ту же сущность создаётся как отдельный Booking с собственным жизненным циклом.
10. cancel_requested — запрошена отмена
Кто-то (клиент, агент, поставщик, платформа) запросил отмену. Платформа начинает обработку.
Инварианты:
cancellation_initiatorзафиксирован.cancellation_reasonзадан (для аналитики и refund policy).- Применяется политика возврата (см. Жизненный цикл пост-бронирования).
Допустимые переходы:
- →
cancel_in_progress_supplier(отмена отправлена поставщику). - →
cancelled(если отмена не требует обращения к поставщику — например, доsupplier_confirmed).
11. cancel_in_progress_supplier — отмена выполняется у поставщика
Запрос на отмену отправлен поставщику; платформа ждёт подтверждения отмены.
Инварианты:
- Запрос к поставщику отправлен.
- Срок ожидания активен.
Допустимые переходы:
- →
cancelled(поставщик подтвердил отмену). - →
unknown_external_state(поставщик не ответил — обработка черезBookingRestorationJob). - →
failed(поставщик отказал в отмене из-за политики — например, неотменяемый тариф; платформа может оспорить или принять).
12. cancelled — отменено
Терминальное состояние успешной отмены.
Инварианты:
- Поставщик подтвердил отмену (или отмена не требовала поставщика).
Refundсоздан и обрабатывается (или платёж был только authorized — тогдаvoidбез возврата).Settlementобновлён или отменён.- Событие
booking.cancelledопубликовано.
Допустимые переходы: нет (terminal state).
13. amendment_in_progress — выполняется изменение
После confirmed запрошено изменение бронирования (даты, состав путешественников, тип номера, и т.д.). Это сложный поток, потому что меняется существующее бронирование, а не создаётся новое.
Инварианты:
- Сохранён
prior_state_snapshotдля отката. - Запрос на изменение отправлен поставщику.
- Применяется политика изменений (некоторые изменения — бесплатные, некоторые — с доплатой).
Допустимые переходы:
- →
platform_confirmed(изменение принято, состояние обновлено; возможно создание дополнительногоPaymentIntentдля доплаты). - →
amendment_failed(изменение отклонено — возврат к prior state). - →
unknown_external_state(поставщик не ответил).
14. completed — исполнено
Терминальное успешное состояние. Дата выезда прошла, бронирование выполнено.
Инварианты:
- Дата выезда (
checkout_date) в прошлом. Settlementпереведён вcleared(выплата поставщику ожидается).- Событие
booking.completedопубликовано. - Window для подачи возвратного платежа (chargeback) ещё активно (типично 60–180 дней) — мониторится отдельно.
Допустимые переходы: нет (terminal state). Возможные пост-обработки (возвратный платёж, спор) обрабатываются как отдельные сущности (Chargeback), не меняют состояние Booking.
Граф переходов с триггерами
Полный граф (ребро = переход с триггером):
draft
└─ user_submit_action → submitted
└─ user_cancel_action → cancel_requested → cancelled (быстро)
submitted
├─ pre_checks_passed_revalidation_required → pending_revalidation
├─ pre_checks_passed_revalidation_skipped → pending_supplier_confirmation
├─ pre_checks_failed → failed
└─ user_cancel_action → cancel_requested
pending_revalidation
├─ supplier_revalidated_offer_actual → pending_supplier_confirmation
├─ supplier_revalidated_offer_outdated → failed
├─ supplier_timeout → unknown_external_state
└─ user_cancel_action → cancel_requested
pending_supplier_confirmation
├─ supplier_callback_confirmed → supplier_confirmed
├─ supplier_callback_partial → partially_confirmed
├─ supplier_callback_rejected → failed
├─ supplier_timeout → unknown_external_state
└─ user_cancel_action → cancel_requested (сложный случай)
supplier_confirmed
└─ platform_invariants_satisfied → platform_confirmed
partially_confirmed
├─ user_accepts_partial → platform_confirmed (с частичной фиксацией платежа)
└─ user_rejects_partial → cancel_requested
platform_confirmed
├─ checkout_date_passed → completed
├─ cancel_initiated → cancel_requested
└─ amend_initiated → amendment_in_progress
unknown_external_state
├─ recovery_clarified_state_pending → pending_supplier_confirmation
├─ recovery_clarified_state_confirmed → supplier_confirmed
├─ recovery_clarified_state_failed → failed
└─ operator_decision → cancel_requested
cancel_requested
├─ requires_supplier_action → cancel_in_progress_supplier
└─ no_supplier_action_required → cancelled
cancel_in_progress_supplier
├─ supplier_callback_cancelled → cancelled
├─ supplier_callback_refused → failed (особый случай — оспаривание)
└─ supplier_timeout → unknown_external_state
amendment_in_progress
├─ supplier_accepted_amendment → platform_confirmed (с обновлёнными параметрами)
├─ supplier_rejected_amendment → amendment_failed → возврат к prior_state (snapshot восстанавливается)
└─ supplier_timeout → unknown_external_state
Все остальные переходы — запрещены на уровне state machine. Попытка сделать запрещённый переход (например, перевести cancelled обратно в confirmed) блокируется с ошибкой invalid_state_transition.
Идемпотентность
Принцип
Каждая операция над бронированием может прийти повторно — из-за тайм-аута и повтора партнёра, из-за повторной доставки события, из-за ручного replay. Платформа обязана обрабатывать повторы корректно — не создавать дубликат, не выполнять операцию дважды.
Каноничный механизм
Все критические операции принимают idempotency_key от вызывающего:
create_booking(idempotency_key, ...)— создание.confirm_booking(idempotency_key, ...)— подтверждение.cancel_booking(idempotency_key, ...)— отмена.amend_booking(idempotency_key, ...)— изменение.complete_booking(idempotency_key, ...)— завершение.
При получении операции:
- Платформа ищет
BookingStateTransitionс тем жеidempotency_keyдля этогоbooking_idза окно дедупликации (типично 24 часа). - Если найдено — возвращается результат предыдущей операции, без повторного выполнения.
- Если не найдено — операция выполняется, ключ записывается.
Идемпотентность с поставщиком
Запросы к поставщику также должны быть идемпотентными. Каждый запрос содержит supplier_request_id (генерируется платформой, передаётся поставщику). При повторе платформой того же запроса — поставщик возвращает результат предыдущего, не создаёт второе бронирование у себя.
Если поставщик не поддерживает идемпотентность — платформа обязана реализовать защиту через свой журнал (не отправлять второй запрос с тем же supplier_request_id, пока не получен ответ на первый).
Защита от двойных списаний
Если операция требует фиксации платежа (PaymentIntent.captured) — фиксация строго один раз через идемпотентность платежа (см. Платёжный домен).
При неудаче (например, тайм-аут после фиксации платежа, но до сохранения состояния platform_confirmed) — повторная попытка обнаруживает, что платёж уже зафиксирован, и просто завершает переход состояния, не повторяя фиксацию.
Различение supplier_confirmed и platform_confirmed
Зачем два состояния
Уже зафиксировано в Семантика предложений, цены, бронирования: «потому что supplier positive response и platform-level commit finalization не всегда являются одним атомарным моментом».
Конкретно, между моментом получения положительного ответа от поставщика и моментом полной готовности бронирования на платформе должны выполниться:
- Фиксация платежа (
PaymentIntent.captured). - Создание
Settlementдля взаиморасчёта. - Применение политики маржи и налогов.
- Публикация доменного события
booking.confirmed. - Запуск пост-обработки (генерация документов клиенту, обновление аналитики, обновление поисковой проекции если применимо, и т.д.).
Каждое из этих действий может занять время или провалиться. Без явного промежуточного состояния supplier_confirmed:
- Если платёж не зафиксировался после ответа поставщика — есть бронирование у поставщика, но нет денег у платформы. Финансовая дыра.
- Если событие не опубликовалось — внутренние контуры не узнают о бронировании. Settlement не создаётся.
- Если пост-обработка не запустилась — клиент не получает подтверждение. Жалобы и chargeback.
Состояние supplier_confirmed явно фиксирует «поставщик уже подтвердил, мы должны довести инварианты». Это критическое состояние для операционного восстановления.
Внешний вид
Для внешних потребителей (партнёров, клиентов, агентов) supplier_confirmed и platform_confirmed объединены под именем confirmed. Различение видно только:
- Операционным командам платформы.
- В аналитике (для расчёта операционных метрик).
- В журнале аудита.
Это намеренное упрощение наружу — клиент не должен думать о двух состояниях.
Каноничные таймауты для supplier_confirmed → platform_confirmed
Переход должен занимать секунды, не минуты. Если переход занимает больше:
- Срабатывает алерт оперативной команде.
- Запускается
BookingRestorationJobдля расследования. - При длительной задержке (более 5 минут) — эскалация.
Эта ситуация — операционный инцидент, не нормальное поведение.
Обработка unknown_external_state
Принцип
Поставщики — внешние системы с разной надёжностью. Тайм-ауты, разрывы соединений, некорректные ответы — реальность операций. Платформа не имеет права «забывать» бронирования с неопределённым состоянием.
unknown_external_state — операционная категория первого класса — состояние с обязательным циклом восстановления.
Восстановительный цикл
Бронирование переходит в unknown_external_state
↓
Создаётся BookingRestorationJob со state_before_restoration
↓
Job планируется к запуску с экспоненциальным откатом:
- Попытка 1: через 1 минуту
- Попытка 2: через 5 минут
- Попытка 3: через 15 минут
- Попытка 4: через 1 час
- Попытка 5: через 6 часов
- Попытка 6: через 24 часа
↓
На каждой попытке:
- Запрос статуса у поставщика по supplier_request_id
- Анализ ответа
↓
Возможные исходы:
├── Поставщик ответил — состояние выяснено → переход к соответствующему состоянию
├── Поставщик не ответил — следующая попытка
└── После 6 попыток (около 32 часов) — эскалация оператору
↓
Эскалация — ручное расследование оператором:
- Звонок поставщику
- Проверка через альтернативные каналы
- Решение по конкретному случаю
↓
Оператор фиксирует решение через manual_override переход
Защита клиента
Во время unknown_external_state клиент не должен страдать:
PaymentIntentостаётся вauthorized(платёж зарезервирован, но не списан) — клиент не теряет деньги.- Клиент уведомляется о ситуации с разъяснением (см. Уведомления и коммуникации).
- При длительной неопределённости (более 24 часов) клиент имеет право на принудительную отмену с полным возвратом резервации.
Журнал для аудита
Все попытки восстановления, все ответы поставщика, все решения оператора — фиксируются в BookingRestorationJob.actions_taken. Это:
- Доказательная база для регулятора при спорах.
- Материал для аналитики качества поставщиков.
- Обучающий материал для будущих случаев.
Жёсткие гарантии завершения цикла unknown_external_state
Раздел добавлен после внешнего архитектурного ревью 30.04.2026, в котором отмечен риск: «если этот статус не имеет жёсткого автоматического тайм-аута с переходом в failed или manual_intervention, система накопит зависшие деньги».
Раздел фиксирует жёсткие гарантии того, что состояние unknown_external_state всегда завершается в конечное (terminal или operationally final) состояние в рамках детерминированных временных окон, и что в течение этого периода не образуется «зависших денег» ни на стороне клиента, ни на стороне партнёра.
Финальный deadline цикла восстановления
Восстановительный цикл (6 попыток с экспоненциальным backoff'ом до ~32 часов) никогда не завершается оставлением бронирования в unknown_external_state. По истечении окна восстановления автоматически выполняется один из переходов:
failed— если в ходе попыток получены явные сигналы неудачи (например, поставщик в одной из попыток вернулnot_foundилиcancelledдляsupplier_request_id).manual_intervention— если поставщик не отвечает или отвечает противоречиво в течение всего окна. Этот переход обязательный, не опциональный.
manual_intervention — это operationally final state в рамках state machine: дальнейшие переходы возможны только через явное действие оператора (manual_override с зафиксированным reason). Бронирование не может оставаться в manual_intervention без зарегистрированного BookingRestorationJob с открытым actions_taken[] и assigned-оператором.
Финансовые гарантии в течение unknown_external_state
В течение всего цикла восстановления и в manual_intervention:
На стороне клиента:
PaymentIntentнаходится вauthorized(зарезервирован), неcaptured(не списан).- Через 24 часа в
unknown_external_stateклиент имеет право на принудительную отмену с полным возвратом резервации (см. Уведомления и коммуникации).
На стороне партнёра (в гибридной merchant-of-record модели):
- Партнёрский баланс не блокируется навсегда за бронированиями в
unknown_external_state. - Каноничное правило: средства, зарезервированные на партнёрском балансе под бронирование, освобождаются автоматически по timeout-окну (см. правило
unknown_state_balance_releaseв partner-finance-and-clearing.md). - Если в момент финального исхода (
failedилиmanual_intervention) выясняется что обязательство всё-таки возникло — баланс корректируется обратно через explicitclearing_correction_eventс audit trail. - Это устраняет риск ситуации «партнёрские деньги заморожены навсегда из-за молчания поставщика».
На стороне платформы (insolvency protection):
- Если бронирование переходит в
manual_interventionи далее вfailed, но при этом end-customer уже совершил платёж — платформа обязана вернуть end-customer'у через нашего партнёра независимо от того, вернул ли поставщик (per Package Travel Directive insolvency protection). - Разница покрывается через loss accounting (см. раздел
Settlement Splitв partner-finance-and-clearing.md).
Связь с SLI и runbook'ом инцидента
Доля бронирований в unknown_external_state — критическая операционная метрика платформы, один из 10 каноничных SLI (см. operations/sla-and-on-call-model.md, метрика unknown_external_state_share).
Целевые значения:
- Premium-уровень: менее 1% от активных бронирований.
- Standard-уровень: менее 3%.
- Free-уровень: best effort.
Триггеры эскалации:
unknown_external_state_share > 1%для премиум-уровня — алертtier_3_oncall(oncall поставщикам и архитекторам ingestion'а).unknown_external_state_share > 5%для любого уровня —tier_1_incident(несколько supplier'ов одновременно ведут себя аномально, возможна инфраструктурная проблема платформы).- Конкретный supplier генерирует более 10% бронирований в
unknown_external_stateза 24 часа — анализ его supplier quality dashboard, возможна временная приостановка ingestion'а от него (см. data-governance-and-matching.md, разделGovernance И Supplier Quality).
Runbook каноничного инцидента unknown_external_state_share_breach:
- Identify: какой supplier даёт наибольший вклад в долю.
- Quarantine: временная приостановка новых бронирований через этого supplier'а, если вклад > 50%.
- Communicate: уведомление аффектированных партнёров о деградации.
- Investigate: контакт с supplier'ом (auto-ticket в их support system + escalation к contractual TAM).
- Recover: после устранения проблемы — запуск ускоренного восстановительного цикла для застрявших бронирований.
Полный runbook — в operations/runbooks-incident-playbooks.md.
Защита от cumulative deadlock'ов
Дополнительная гарантия: общее число открытых BookingRestorationJob в системе ограничено сверху (max_concurrent_restoration_jobs, по умолчанию 10000). При достижении лимита:
- Новые бронирования, попадающие в
unknown_external_state, немедленно переходят вmanual_interventionбез проходжения восстановительного цикла. - Это circuit breaker на уровне платформы: если что-то системно сломалось и одновременно «застряли» десятки тысяч бронирований, платформа не пытается auto-restore все из них (создаст лавину запросов к supplier'ам), а передаёт оператору.
- Лимит — конфигурируемый, обновляется в runtime без redeploy.
Это исключает сценарий «системный бот платформы DDOS'ит supplier'а попытками восстановления» при инфраструктурной проблеме.
Каноничные таймауты для unknown_external_state — обновлённые
Уточнения к таблице каноничных таймаутов раздела Каноничные таймауты ниже:
| Состояние | Hard timeout | Финальный исход |
|---|---|---|
unknown_external_state (search-флоу) | 15 минут | → failed или manual_intervention |
unknown_external_state (booking-флоу, до confirmed) | 4 часа | → failed или manual_intervention |
unknown_external_state (post-confirmed, например cancel/amendment in progress) | 32 часа (восстановительный цикл из 6 попыток) | → manual_intervention |
manual_intervention | нет timeout, но есть SLA на assigned-оператора (8 часов) | → resolution через manual_override |
Search-флоу 15 минут и booking-флоу 4 часа — это hard caps, после которых автоматический переход обязателен. Расширение восстановительного цикла до 32 часов применяется только для уже подтверждённых бронирований (post-confirmed), где cancel/amendment не получил подтверждения от supplier'а — там быстрая эскалация недопустима, потому что у клиента уже есть подтверждённый booking, и преждевременный переход в failed создаст финансовые последствия.
Обоснование (тезисы)
Тезис 1. unknown_external_state обязан завершаться в детерминированный конечный исход.
Альтернативы: (а) оставлять бронирование в unknown_external_state сколь угодно долго; (б) hard timeout с автоматической эскалацией.
Trade-off: вариант (а) приводит к деградации SLI и накоплению «зависших денег», что прямо отметил внешний ревьюер; вариант (б) — каноничный, обеспечивает predictable behavior.
Тезис 2. Финансовое состояние партнёра не должно блокироваться внутри неопределённости поставщика.
Альтернативы: (а) блокировать партнёрский баланс до итогового исхода; (б) освобождать баланс по timeout с возможной коррекцией; (в) не резервировать баланс вообще до confirmed.
Trade-off: вариант (а) делает партнёра заложником ненадёжности поставщика; вариант (в) допускает overdraft партнёром в момент confirmed; вариант (б) — каноничный баланс защиты партнёра и финансовой корректности.
Тезис 3. Cumulative circuit breaker на уровне платформы предотвращает каскадные сбои.
Альтернативы: (а) безлимитное число параллельных restoration jobs; (б) hard limit с обходом через manual_intervention.
Trade-off: вариант (а) при инфраструктурной проблеме создаёт лавину запросов к supplier'ам, ухудшая ситуацию; вариант (б) — каноничный паттерн bulkhead/circuit breaker.
Replay-safe механизмы
Восстановление через журнал событий
Все доменные события бронирования (booking.submitted, booking.supplier_confirmed, booking.platform_confirmed, booking.cancelled, и т.д.) — публикуются в каноничный журнал событий.
При сбое сервиса бронирования (потеря состояния в памяти, краш) текущее состояние любого бронирования полностью восстанавливается воспроизведением событий из журнала. Каждое событие применяется к state machine, давая итоговое состояние.
Это критическое свойство устойчивости:
- Невозможно «потерять» бронирование из-за сбоя сервиса.
- Корректность state machine гарантируется математически — операции применения событий коммутативны и идемпотентны в правильном порядке.
- Возможен debug — реконструкция любого исторического состояния по любой временной точке.
См. Событийная шина и асинхронная дисциплина для общих правил replay.
Идемпотентность применения событий
При повторной доставке события — состояние не меняется (если событие уже применено). Контрольная точка — last_applied_event_id на сущности Booking.
Снимки состояния (state snapshots)
Для оптимизации — периодические снимки состояния бронирования. При восстановлении — replay только событий после последнего снимка. Это снижает время восстановления для бронирований с длинной историей.
Каноничные таймауты
Каждое ожидающее состояние имеет таймаут:
| Состояние | Таймаут | Действие при истечении |
|---|---|---|
submitted | 30 секунд (для синхронных pre-checks) | → failed (pre_checks_timeout) |
pending_revalidation | 60 секунд | → unknown_external_state |
pending_supplier_confirmation | 5 минут (синхронные поставщики), до 24 часов (асинхронные с явной seller policy) | → unknown_external_state |
supplier_confirmed → platform_confirmed | 5 минут (нормально — секунды) | → алерт оперативной команде, restoration job |
partially_confirmed (ожидание решения клиента) | 30 минут (B2C), 24 часа (B2B) | → cancel_requested (по политике) |
cancel_in_progress_supplier | 5 минут (типично) | → unknown_external_state |
amendment_in_progress | 5 минут | → unknown_external_state |
Таймауты — конфигурируемые через tenant_configuration для определённых тенантов (например, корпоративные могут иметь другие SLA).
Запрещённые переходы
State machine блокирует следующие классы переходов:
- ❌ Любой переход из terminal state (
failed/cancelled/completed). - ❌ Переход назад во времени (например,
confirmed → submitted). - ❌ Переход через несовместимое состояние (например,
submitted → completedбез прохожденияconfirmed). - ❌ Переход без выполнения предусловий (попытка
pending_supplier_confirmation → supplier_confirmedбез получения ответа от поставщика). - ❌ Переход из
supplier_confirmedнапрямую вfailed(нарушение финансовой целостности — мы должны клиенту услугу). - ❌ Прямой переход в
platform_confirmedминуяsupplier_confirmed(нарушение каноничного порядка).
Попытка запрещённого перехода:
- Возвращает ошибку
invalid_state_transitionс указанием текущего состояния и запрошенного. - Журналируется как
BookingInvariantCheckс severityerror. - При систематических попытках — эскалация (возможна ошибка в коде).
Стыковка с платёжным контуром
Полная таблица стыковки — в Платёжный домен, здесь — обратная сторона:
Состояние Booking | Допустимое состояние PaymentIntent |
|---|---|
draft | created или отсутствует |
submitted | created / selecting_psp / awaiting_method |
pending_revalidation | awaiting_method или processing |
pending_supplier_confirmation | authorized (платёж зарезервирован) |
supplier_confirmed | authorized (фиксация в процессе) |
platform_confirmed | succeeded (фиксация завершена) |
partially_confirmed | authorized (ожидание решения клиента) |
unknown_external_state | authorized (НЕ succeeded до выяснения) |
failed | voided или refunded (если фиксация была) |
cancel_requested | authorized или succeeded (зависит от стадии) |
cancel_in_progress_supplier | authorized или succeeded |
cancelled | voided (без списания) или refunded (с возвратом) |
amendment_in_progress | succeeded (предыдущий) + новый PaymentIntent для доплаты при необходимости |
completed | succeeded (может быть partially_refunded после частичной отмены) |
При любом нарушении этой стыковки — BookingInvariantCheck с invariant_class: payment_state_consistent срабатывает и блокирует переход.
Стыковка с сагой пакетного тура
См. Операционная модель конструктора туров для деталей TourBookingTransaction.
Каждый компонент пакетного тура — отдельный Booking со своим жизненным циклом. Сага координирует их атомарность:
- При успехе всех компонентов — все переходят в
platform_confirmedсинхронно. - При провале одного — выполняются компенсирующие переходы для уже подтверждённых.
Каноничное поле BookingStateTransition.saga_step_id фиксирует, какой шаг саги вызвал переход. Это даёт полный аудит распределённой транзакции через журнал.
События статусной машины бронирования
Каноничный набор событий, публикуемых при переходах:
| Событие | Когда | Главные потребители |
|---|---|---|
booking.submitted | Переход в submitted | Контур валидации, аналитика |
booking.revalidation_required | Переход в pending_revalidation | Контур наблюдаемости |
booking.supplier_request_sent | Запрос отправлен поставщику | Контур наблюдаемости поставщика |
booking.supplier_confirmed | Переход в supplier_confirmed | Платёжный контур (для запуска фиксации) |
booking.confirmed | Переход в platform_confirmed | Все потребители — пост-бронирование, settlement, аналитика, уведомления |
booking.partially_confirmed | Переход в partially_confirmed | Контур уведомлений (запрос решения клиента) |
booking.unknown.external.state | Переход в unknown_external_state | Срочный алерт оперативной команде, restoration scheduler |
booking.restoration_attempted | Каждая попытка восстановления | Контур наблюдаемости |
booking.restoration_clarified | Восстановление выяснило состояние | Контур наблюдаемости |
booking.cancel_requested | Переход в cancel_requested | Платёжный контур, контур уведомлений |
booking.cancelled | Переход в cancelled | Платёжный контур (для refund), settlement, аналитика |
booking.amendment_started | Переход в amendment_in_progress | Контур пост-бронирования |
booking.amendment_succeeded | Возврат в platform_confirmed после изменения | Контур пост-бронирования |
booking.amendment_failed | Возврат к prior state | Контур уведомлений |
booking.failed | Переход в failed | Контур наблюдаемости, контур уведомлений |
booking.completed | Переход в completed | Settlement (запуск выплаты), аналитика |
booking.invariant_violation_detected | Обнаружено нарушение инварианта | Срочный алерт оперативной команде |
Полная таксономия — в Первоначальная таксономия событий и в JSON Schema Like Field Catalogs.
Метрики и наблюдаемость
Каноничные метрики state machine
- Распределение по состояниям — сколько бронирований в каждом состоянии в реальном времени.
- Среднее время в каждом состоянии — индикатор операционной скорости. Долгое нахождение в
pending_supplier_confirmation— сигнал проблем у поставщика. - Доля переходов в
unknown_external_state— главная метрика устойчивости. Цель — менее 1%. - Доля успешного восстановления из
unknown_external_state— цель более 95%. - Среднее время восстановления — операционный показатель.
- Доля бронирований, проходящих весь путь до
completed— главная продуктовая метрика. - Доля бронирований, проходящих через
partially_confirmed— продуктовый сигнал (плохое качество доступности у поставщиков). - Распределение причин
failed— для оптимизации pre-checks. - Доля переходов через
amendment_in_progress— продуктовый сигнал (как часто клиенты меняют бронирования).
Алерты
Через Аналитика и бизнес-аналитика и Наблюдаемость и реагирование на инциденты:
- Алерт при доле
unknown_external_stateвыше 2% за час — оперативная команда расследует. - Алерт при превышении таймаута
supplier_confirmed → platform_confirmed(более 5 минут) — критический инцидент. - Алерт при систематических
BookingInvariantCheckнарушениях для конкретного типа — возможна ошибка кода. - Алерт при росте
failedс конкретной причиной — возможна деградация поставщика или политики.
Фазы развёртывания state machine
Фаза Bootstrap (0–6 месяцев)
Что разворачивается:
- Каноничная модель
Booking,BookingStateTransition,BookingInvariantCheck,BookingRestorationJobзафиксирована в схеме хранения. - Базовая state machine с упрощённым набором состояний для первого тенанта (vitrip.store).
- Идемпотентность критических операций.
- Простой механизм восстановления для
unknown_external_state(без расширенной автоматики). - Базовая публикация событий бронирования.
Триггер выхода:
- Готовность к partner production launch — нужна полная зрелая state machine.
Фаза 2 — Production state machine (6–18 месяцев)
Что разворачивается:
- Все 14 каноничных состояний.
- Полные инварианты на каждом переходе.
- Расширенный механизм восстановления с экспоненциальным откатом.
- Replay-safe журнал событий с возможностью полного восстановления состояния.
- Стыковка со state machine
PaymentIntent. - Каноничные таймауты для каждого состояния.
- Журнал переходов с идемпотентностью.
Триггер выхода:
- Появление пакетных туров через сагу.
- Появление амэндмэнтов на масштабе.
Фаза 3 — Smart booking (18–30 месяцев)
Что разворачивается:
- Полная стыковка с сагой пакетного тура.
- Расширенный амэндмэнт-flow.
- ML-модели прогнозирования провалов (anomaly detection в state machine).
- Автоматическая эскалация на основе паттернов.
- Расширенная аналитика state machine для тенантов (Professional+).
Фаза 4 — Multi-region (30+ месяцев)
Что разворачивается:
- Multi-region обработка с локализацией данных.
- Federated state machine для крупных корпоративных партнёров.
- Расширенная защита от регионально-специфичных видов сбоев.
Архитектурные решения с тезисным обоснованием
Решение 1. 14 явных состояний, не упрощённая модель
Цель: обеспечить точную обработку каждой операционной ситуации.
Тезисы:
- Упрощённые модели (например, 4 состояния: pending / confirmed / cancelled / failed) не отражают реальности работы с поставщиками — теряются состояния
partially_confirmed,unknown_external_state,amendment_in_progress, что приводит к ad-hoc обработке и ошибкам. - Каждое из 14 состояний — операционно различимо и требует разной обработки.
- Современные платформы (Stripe, Booking.com, Airbnb) — все строят детальные state machines в финансовых контурах. Это база.
Решение 2. Различение supplier_confirmed и platform_confirmed
Цель: разделить момент ответа поставщика и момент полной готовности бронирования на платформе.
Тезисы:
- Без различения — финансовые дыры при сбое между ответом поставщика и фиксацией платежа.
- Различение даёт явный operational moment для аудита и восстановления.
- Внешнее упрощение под
confirmedсохраняет user experience.
Решение 3. unknown_external_state — first-class категория
Цель: обеспечить устойчивость при ненадёжных внешних поставщиках.
Тезисы:
- Тайм-ауты и неопределённые ответы поставщиков — реальность, не исключение. Без явного состояния — ad-hoc обработка с потерями.
- Явная категория с восстановительным циклом — единственный способ дать гарантии клиенту.
- Современные платформы с распределёнными зависимостями (Uber, Stripe, AWS) — все имеют явные unknown states. Это база.
Решение 4. Идемпотентность через idempotency_key для всех критических операций
Цель: защита от двойных списаний и дубликатов при повторах.
Тезисы:
- Сетевые сбои и повторы — реальность партнёрской интеграции. Без идемпотентности — двойные списания и дубликаты.
- Идемпотентность через ключ — стандарт индустрии (Stripe, Square).
- Архитектурное решение защищает независимо от добросовестности партнёрского кода.
Решение 5. Replay-safe через журнал событий
Цель: обеспечить полное восстановление состояния при сбое любого компонента.
Тезисы:
- Восстановление из БД-снимка может потерять in-flight операции.
- Журнал событий с воспроизведением — единственный способ полного восстановления.
- Замена сервиса бронирования (rollback версии) не приводит к потере данных — события переигрываются.
Открытые развилки
Развилка 1. Тонкая настройка таймаутов для разных классов поставщиков
Высокотехнологичные поставщики отвечают за секунды; legacy XML-поставщики могут отвечать минутами. Тайм-ауты должны быть различны для разных классов поставщиков. Каноничная классификация и таймауты — открытая развилка.
Эскалируется: при подключении второго поставщика с другим профилем задержки.
Развилка 2. Автоматическая обработка некоторых случаев unknown_external_state через ML
ML-модель может предсказывать, в каком конечном состоянии окажется бронирование (на основе профиля поставщика, времени суток, типа продукта). Применение к автоматическому решению — открытая развилка фазы 3.
Эскалируется: при создании ML-стратегии в Платформа машинного обучения.
Развилка 3. Расширенный амэндмэнт-flow
Текущая модель поддерживает базовое изменение через возврат к prior_state при провале. Сложные амэндмэнты (изменение нескольких параметров, цепочки амэндмэнтов) — открытая развилка.
Эскалируется: при появлении партнёрских запросов на сложные сценарии.
Развилка 4. Поддержка частичных компенсаций в саге
Текущая модель компенсации в саге — полный откат всех успешных компонентов. Альтернатива — частичные компенсации (попытка сохранить часть и компенсировать только провалившиеся). Сложнее, но даёт лучший пользовательский опыт.
Эскалируется: при оптимизации опыта в фазе 3.
Развилка 5. Time-shifted бронирования
Некоторые поставщики позволяют забронировать с отсроченной датой подтверждения (например, через 24 часа после запроса). Это требует расширенного state machine с дополнительным состоянием reservation_held_pending_supplier_decision. Текущая модель упрощённо относит к pending_supplier_confirmation с длинным таймаутом.
Эскалируется: при подключении поставщиков с такой моделью.
Связанная документация
Корневые архитектурные документы
- Каноничная доменная ось — каноничная сущность
Booking. - Поверхности взаимодействия — поверхности, на которых видны состояния бронирования.
- Платформа как продукт — стабильность контракта внешних состояний.
- Манифест переосмысления — обязательство первоклассной обработки
unknown_external_state. - Операционная ось — наблюдаемость state machine.
Связанные доменные документы
- API Contracts — внешний контракт состояний
Booking Surface. Этот документ детализирует тезисный список оттуда. - Семантика предложений, цены, бронирования — обоснование различения
supplier_confirmed/platform_confirmed. - Business Services — Booking Service.
- Платёжный домен — стыковка с
PaymentIntentstate machine. - Операционная модель конструктора туров — сага
TourBookingTransaction, использующая state machineBooking. - Жизненный цикл пост-бронирования — пост-обработка после
confirmed. - Партнёрские взаиморасчёты — settlement после
confirmed. - Поставщики — классификация поставщиков, профили задержки.
- Тенантная настройка — конфигурация таймаутов на тенант.
- Учёт потребления и квоты — учёт операций бронирования.
- Уведомления и коммуникации — каналы уведомлений по событиям бронирования.
- Аналитика и бизнес-аналитика — витрина воронки бронирования.
- Платформа машинного обучения — ML-применения над state machine.
Документы развития
- Первоначальная таксономия событий — таксономия событий бронирования.
- Скелеты AsyncAPI и event envelopes — формальная спецификация событий.
- JSON Schema Like Field Catalogs For Top Critical Events — детальные схемы событий бронирования.
- Каталог каналов событий — каналы событий бронирования RU-5.
- Реестр исполнения готовности — Slice D «Booking Commit and Recovery».
- Реестр повторов, очередей мёртвых писем и replay — политики повторов.
- Техническое задание для подрядчика — § 3.4 транзакционный слой бронирования.
Операционная сторона
- Наблюдаемость и реагирование на инциденты — наблюдаемость state machine, алерты.
- Релизы и совместимость — стабильность контракта при эволюции state machine.
- Дорожная карта инфраструктурного масштабирования — фазы развёртывания.
Архитектурные правила
- Закон 00000 — платформа главенствует над поставщиками — каноничная state machine платформы, не поставщиков.
- Современные лучшие практики верхнеуровневых платформ — Stripe, Booking.com, Airbnb как ориентиры финансовых state machines.
- Развитие без деградации — фазы как расширение, не миграция.
- Эластичное масштабирование и упаковка по фазам — фазы state machine.
- Тезисное обоснование архитектурных решений — формат принятия решений.