Перейти к основному содержимому

Операционная модель конструктора туров

Версия: 1.0 Дата: 26.04.2026 Статус: Готов к обсуждению

Назначение документа

Документ определяет операционную модель конструктора туров (Tour Builder operational model) Vitiana — как именно пользователь (агент агентства, партнёр через программный интерфейс, конечный клиент через потребительскую витрину) собирает многокомпонентный тур из черновика до подтверждённого пакета. Включает:

  • Каноничные правила композиции (composition rules) — что и с чем совместимо, что обязательно, что опционально.
  • Поток состояний черновика тура (TourDraft) от создания до публикации.
  • Обработку расхождения цен и доступности (drift handling) между моментом добавления компонента в черновик и моментом публикации.
  • Разрешение конфликтов вариантов (variant conflicts).
  • Версионирование и журнал изменений черновика.
  • Атомарность бронирования пакетного тура — как обеспечивается «всё или ничего» при подтверждении тура из нескольких компонентов.
  • Тип-1 / тип-2 / тип-3 связки сегментов — каноничная классификация типов тура.
  • Партнёрскую модель интеграции — как партнёр строит тур через программный интерфейс.
  • Интеграцию с другими доменами (поиск, платежи, пост-бронирование, медиа).
  • Метрики операционной модели и наблюдаемость.
  • Регуляторные следствия (Директива о пакетных турах — Package Travel Directive 2015/2302).
  • Фазы развёртывания.

Документ читается после:

В корневых документах зафиксировано что Tour Builder делает и где живёт. Этот документ описывает как — операционные правила, поток состояний, drift handling, атомарность.

Tour Builder — главная архитектурная новизна Vitiana. Большинство платформ — каталоги отелей с возможностью бронирования (один отель за раз). Tour Builder позволяет в одной операции собрать сложный тур из нескольких сегментов — проживание в нескольких городах, переезды между ними, экскурсии, страховку. Это основа дифференциации платформы.

Реальное состояние реализации (home-to-go-api): пока работает поиск и бронирование одного отеля. Tour Builder как операционная модель — новая зона, требующая собственной реализации.

Правило 00000 (платформа главенствует над поставщиками) применяется здесь напрямую: правила композиции тура — каноничные правила платформы, не наследие от поставщиков. Поставщик отеля не диктует, какие переезды совместимы с проживанием в его отеле; платформа сама задаёт правила совместимости компонентов разных типов от разных поставщиков.

Главное решение

Tour Builder — closed-system raw API constructor по модулям с явными состояниями черновика, явными правилами композиции и атомарным подтверждением пакетного тура.

Это означает:

  • Закрытая система — программный интерфейс Tour Builder не открыт всем партнёрам; доступен только тарифам с явной поддержкой (Professional с paid Tour Builder access, Enterprise).
  • Raw API constructor по модулям — партнёр оперирует примитивами композиции (добавить сегмент, заменить вариант, проверить совместимость), а не «магической функцией построения тура». Это даёт максимальную гибкость и контроль.
  • Явные состояния черновикаTourDraft имеет каноничную статусную машину; никаких «случайных» состояний.
  • Правила композиции — каноничные сущности — не зашиты в код, а конфигурируются через CompositionRule.
  • Drift handling — first-class — каждое изменение цены или доступности компонента после добавления в черновик отслеживается и обрабатывается явно.
  • Атомарность пакетного тура — подтверждение тура из нескольких компонентов либо проходит полностью, либо не проходит вообще. Промежуточные состояния (часть забронирована, часть нет) недопустимы.
  • Партнёрский интерфейс — стабильный контракт — изменение правил композиции не ломает существующих партнёров.

Каноничные сущности (углубление существующих)

Уже зафиксированные сущности

В Tour Builder Domain уже зафиксированы:

  • TourDraft — черновик тура.
  • TourDraftItem — элемент черновика (один сегмент).
  • TourVariant — вариант компонента в сегменте (несколько вариантов отеля для одной даты).
  • TourProposal — оформленное предложение тура клиенту.
  • TourVersion — версия черновика (с историей изменений).
  • TourArtifact — артефакт публикации (сгенерированный документ, презентация).

Этот документ не дублирует эти определения, а расширяет операционными сущностями.

Новые операционные сущности

CompositionRule — правило композиции

Сущность, представляющая именованное правило совместимости и композиции компонентов тура.

{
rule_id: UUID,
rule_key: string,
rule_class: enum,
applicable_segment_types: array,
applicable_segment_pairs: array,
rule_logic: object,
severity: enum,
applicable_tenants: array,
applicable_phases: array,
is_active: boolean,
created_at: timestamp
}

Поля:

  • rule_class — класс правила:
    • temporal_consistency — временная согласованность (даты сегментов не пересекаются, нет разрывов больше N часов между сегментами без явного намерения).
    • geographic_consistency — географическая согласованность (между сегментами в разных городах должен быть переезд; обратное неверно).
    • mandatory_pairing — обязательное сочетание (если есть международный сегмент, обязательна страховка).
    • mutual_exclusion — взаимное исключение (несовместимые компоненты).
    • capacity_consistency — согласованность по числу путешественников (все сегменты должны быть на одинаковое число гостей).
    • legal_consistency — юридические правила (например, по Директиве о пакетных турах — пакет должен иметь >= 2 разных компонента).
    • quality_threshold — пороги качества (например, для премиум-тура — все компоненты с минимальным quality_score).
  • severity — серьёзность нарушения (error — блокирует публикацию; warning — отображается, но не блокирует; info — для информации).

CompositionViolation — нарушение правила композиции

Сущность, представляющая конкретное нарушение правила в конкретном TourDraft.

{
violation_id: UUID,
draft_id: UUID,
rule_id: UUID,
triggered_by_item_ids: array,
detected_at: timestamp,
resolved_at: timestamp,
resolution: enum,
display_message_payloads: object,
severity: enum
}

Поля:

  • triggered_by_item_ids — какие элементы черновика вызвали нарушение.
  • resolution — как разрешено (item_replaced / item_removed / dates_adjusted / tour_abandoned / manual_override для случаев corporate Enterprise).
  • display_message_payloads — многоязычные сообщения об ошибке для пользователя.

DriftEvent — событие расхождения

Сущность, представляющая факт изменения цены или доступности компонента после добавления в черновик.

{
drift_id: UUID,
draft_id: UUID,
item_id: UUID,
drift_class: enum,
detected_at: timestamp,
previous_value: object,
current_value: object,
significance: enum,
resolution: enum,
user_notified_at: timestamp,
user_decision_at: timestamp,
user_decision: enum
}

Поля:

  • drift_class — класс расхождения:
    • price_increased — цена выросла.
    • price_decreased — цена упала (обычно благоприятное, но требует пересчёта).
    • availability_lost — компонент стал недоступен.
    • availability_partial — частичная доступность (например, не все номера).
    • policy_changed — изменилась политика отмены или другие условия.
    • metadata_changed — изменилось что-то в описании компонента.
  • significance — значимость (negligible — менее 1% изменения цены; noticeable — 1–5%; material — более 5% или потеря доступности).
  • resolution — как разрешено (auto_accepted — для negligible; user_confirmed — пользователь явно подтвердил; user_rejected — пользователь отказался; auto_replaced — автоматическая замена на эквивалентный вариант для определённых тарифов).

TourBookingTransaction — транзакция бронирования пакета

Сущность, представляющая атомарную транзакцию подтверждения многокомпонентного тура.

{
transaction_id: UUID,
draft_id: UUID,
proposal_id: UUID,
state: enum,
component_bookings: array,
saga_step: enum,
saga_log: array,
payment_intent_id: UUID,
rollback_required: boolean,
rollback_log: array,
started_at: timestamp,
completed_at: timestamp,
failed_at: timestamp,
failure_reason: string
}

Поля:

  • component_bookings — массив идентификаторов отдельных бронирований по каждому компоненту тура.
  • saga_step — текущий шаг саги (см. секцию атомарности).
  • saga_log — журнал шагов саги для аудита и replay.
  • rollback_required — нужен ли откат при провале одного из компонентов.
  • rollback_log — журнал откатов (какие компоненты успели забронироваться и были отменены).

Связь с другими каноничными сущностями

  • CompositionRule ссылается на Tenant через applicable_tenants — разные тенанты могут иметь разные правила (например, для корпоративного партнёра — особые правила).
  • DriftEvent.item_id — ссылка на TourDraftItem из Tour Builder Domain.
  • TourBookingTransaction.payment_intent_id — единое платёжное намерение для всего пакета (см. Платёжный домен).
  • TourBookingTransaction.component_bookings — отдельные Booking сущности для каждого компонента.
  • Quote фиксируется на уровне всего тура (см. ниже секцию ценообразования пакета), не отдельных компонентов.

Каноничная классификация типов тура

Платформа поддерживает три типа компоновки тура с разной семантикой:

Тип 1. Линейный тур (linear tour)

Несколько сегментов разных типов (проживание + переезд + экскурсия) в одной географической последовательности.

Пример: «3 дня Прага в отеле A + переезд поездом + 4 дня Карловы Вары в отеле B».

Правила:

  • Сегменты идут строго по времени без пересечений.
  • Между сегментами разных географических точек обязателен переезд.
  • Все сегменты на одно и то же число путешественников.

Тип 2. Тематический тур (themed tour)

Тур с центральной темой (свадьба, корпоратив, лечение) и набором согласованных компонентов.

Пример: «свадебный тур — отель с залом + кейтеринг + транспорт для гостей + фотограф».

Правила:

  • Центральный компонент (главное событие) обязателен.
  • Дополнительные компоненты опциональны.
  • Все компоненты привязаны к единой дате центрального события.

Тип 3. Пакетный тур (package tour)

Готовый шаблон тура от платформы или партнёра с фиксированным набором компонентов и параметрами.

Пример: «Пражский weekend-pack: отель 4* + завтраки + обзорная экскурсия + трансфер из аэропорта».

Правила:

  • Шаблон фиксирован, но конкретные варианты компонентов выбираются под даты.
  • Стоимость рассчитывается как сумма с возможной скидкой от платформы.
  • Шаблоны управляются через TourTemplate (отдельная сущность, в Tour Builder Domain).

Различение для Директивы о пакетных турах (EU 2015/2302)

По Директиве:

  • «Package» — комбинация >= 2 разных типов туристических услуг (проживание + переезд, проживание + экскурсия и т.д.) в одной транзакции от одного продавца.

Все три типа платформенного тура с двумя или более компонентами разных типов попадают под Директиву и требуют:

  • Преддоговорной информации (pre-contractual information).
  • Финансовой гарантии (insolvency protection) платформы или продавца-тенанта.
  • Право клиента на возврат при существенных изменениях (см. drift handling material).
  • Ответственность продавца (платформа или тенант — зависит от модели отвечающего за платежи продавца, см. Соответствие требованиям регуляторов).

Поток состояний черновика тура

Каноничная статусная машина TourDraft:

created — пользователь начал композицию

in_composition — добавляются и удаляются элементы

(любое изменение → CompositionRule валидация → возможны CompositionViolation)

validated — все правила прошли, готов к фиксации цены

priced — цены каждого компонента зафиксированы (Quote на каждый компонент)

proposed — TourProposal сгенерирован (документ для клиента)

├── client_review — пользователь / клиент рассматривает
│ ↓
│ ├── approved — клиент согласен, переход к подтверждению
│ └── rejected — клиент отказался

└── direct_commit — без отдельного клиентского одобрения (для агента, оформляющего сразу)

committing — выполняется TourBookingTransaction

├── confirmed — все компоненты успешно забронированы
├── partial_failure — один компонент провалился, ведётся rollback
│ ↓
│ rolled_back — все ранее успешные компоненты отменены

└── permanent_failure — невозможно завершить

Переходы и операции

created → in_composition

Когда: после первого add_item запроса.

in_composition → validated

Когда: пользователь явно вызвал validate_composition или автоматически перед request_pricing.

Действия:

  1. Применяются все активные CompositionRule.
  2. Создаются CompositionViolation для нарушений.
  3. Если есть нарушения уровня error — переход в validated блокируется, остаёмся в in_composition.
  4. Только при отсутствии error-нарушений — переход.

validated → priced

Когда: пользователь вызвал request_pricing.

Действия:

  1. Для каждого TourDraftItem создаётся Quote (через стандартный поток, см. Семантика предложений, цены, бронирования).
  2. Quote'ы привязаны к черновику через специальный идентификатор tour_draft_id.
  3. Сумма цен компонентов плюс маржа платформы (для пакетных туров) даёт общую стоимость тура.

priced → proposed

Когда: пользователь вызвал generate_proposal.

Действия:

  1. Генерируется TourProposal — документ для клиента.
  2. Создаётся TourArtifact (PDF или другая форма) с полным описанием тура.
  3. Документ доставляется клиенту (через Уведомления и коммуникации, типично email).

proposed → client_review или direct_commit

Зависит от поверхности:

  • Tour Builder Closed Surface — типично direct_commit (партнёр знает, что клиент уже согласен).
  • Agency Working Surface — типично client_review, агент ждёт подтверждения клиента.
  • B2C Storefront Surface — типично client_review через интерактив на сайте.

committing → confirmed

Атомарное подтверждение всех компонентов через TourBookingTransaction (см. ниже).

Запрет на «зомби»-черновики

Черновики, не доведённые до публикации в течение N дней (типично 7 для конечных клиентов, 30 для агентов), помечаются как expired и архивируются. Это защищает:

  • От накопления устаревших данных.
  • От использования устаревших цен (Quote истекают раньше).
  • От потенциальных регуляторных рисков (необработанные пакеты, формально подпадающие под Директиву).

Drift handling — обработка расхождения цен и доступности

Главный принцип

Между моментом добавления компонента в черновик и моментом подтверждения тура проходит время. За это время цена компонента может измениться (drift), доступность может пропасть, политики могут измениться.

Платформа обязана отслеживать все такие изменения и информировать пользователя. Молчаливое использование устаревших данных — недопустимо.

Поток обнаружения

TourDraftItem добавлен в черновик

Платформа подписывает item на каноничные события источника:
- offer.price_changed
- offer.availability_changed
- offer.policy_changed
- offer.withdrawn

Событие приходит — создаётся DriftEvent

Расчёт significance:
- изменение менее 1% — negligible
- 1–5% — noticeable
- более 5% или потеря — material

Применение политики разрешения:
- negligible → auto_accepted (без уведомления)
- noticeable → user_notified, требуется явное подтверждение
- material → user_notified, обязательное явное решение

Уведомление пользователю через все доступные каналы

Пользователь решает:
- принять новую цену → DriftEvent.user_decision = accepted, item обновляется
- отказаться → user_decision = rejected, item помечается требующим замены
- заменить вариант → новый TourVariant выбирается, item обновляется

Если есть нерешённые material drifts — переход в priced/proposed заблокирован

Политика автоматических замен (для определённых тарифов)

Для тарифов Professional и Enterprise — опциональная политика автоматической замены при material drift:

  • Платформа автоматически предлагает эквивалентный вариант с похожими параметрами.
  • Сохраняется на основе ML-модели похожести (см. Платформа машинного обучения).
  • Пользователь получает уведомление с предложенным вариантом и опцией принять/отклонить/выбрать другой.

Для базовых тарифов — только пользовательское решение, без автоматической замены.

Защита от спекулятивного использования

Drift handling не позволяет партнёру:

  • «Заморозить» цену простым удержанием черновика — Quote истекает по своему сроку независимо от черновика.
  • Получить arbitrage между ценой в момент добавления и моментом подтверждения — ценовая фиксация на уровне Quote, а не TourDraftItem.

Каскадный drift и auto-replace для смежных компонентов

Раздел добавлен после внешнего архитектурного ревью 30.04.2026, в котором отмечено: «Если в туре из 5 элементов изменилась цена одного, это может сделать невыгодным весь тур. Не хватает логики автоматического переподбора при дрифте, которая была бы бесшовной для агента».

Раздел расширяет уже описанный механизм Drift handling сценарием каскадного влияния: дрейф одного элемента тура может потребовать пересмотра остальных элементов — либо для сохранения экономической целесообразности, либо для сохранения логистической целостности (bundle integrity).

Принцип

Тур — это связанная композиция, не сумма независимых элементов. Дрейф одного элемента может:

  • сделать тур экономически невыгодным (например, авиабилет подорожал на 30%, и теперь общий тур дороже чем альтернативный отдельный пакет);
  • нарушить логистическую связность (например, изменилось расписание авиарейса, и теперь transfer не успевает к новому времени);
  • сделать ineligible применённую коммерческую политику (например, total tour price упал ниже минимального threshold для commission-based pricing партнёра).

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

Каскадный анализ — поток

DriftEvent создан для item X

Запускается CascadeAnalysisJob для всего тура:
Шаг 1: пересчёт total_price тура с новой ценой item X
Шаг 2: проверка bundle integrity:
- время рейса vs время transfer
- регион проживания vs регион activity
- capacity transfer vs party_size
- period purchase insurance vs trip dates
Шаг 3: оценка экономической целесообразности:
- сравнение new_total с original_total
- сравнение new_total с estimated_alternative_packages
Шаг 4: классификация cascade severity:
- cascade_negligible (< 5% impact на общую цену И bundle integrity ОК)
- cascade_noticeable (5–15% impact ИЛИ minor integrity issue решаемый replacement)
- cascade_material (> 15% impact ИЛИ major integrity break ИЛИ компонент unavailable)

Применение политики разрешения cascade

Каноничные threshold'ы

Cascade severityTotal price impactBundle integrityAuto-action
cascade_negligible< 5%OKauto_accept без уведомления
cascade_noticeable5–15%OK или minorauto_replace с уведомлением (для Professional/Enterprise tariffs); user_notified для базовых
cascade_material> 15% или unavailablemajor breakтребуется явное решение пользователя

Threshold'ы конфигурируемые на уровне tenant_configuration — премиум партнёры могут получать более низкий порог auto_accept (более агрессивный auto-replace).

Replacement candidates — ranked alternatives

Для каждого item в туре в момент его добавления в черновик платформа предзаранее подготавливает ranked-список replacement candidates (10-20 alternatives). Кандидаты сохраняются в TourDraftItem.replacement_candidates[] и обновляются:

  • При каждом drift event на этом item.
  • При запуске revalidation тура.
  • По расписанию (каждые N часов в течение жизни черновика).

Источник кандидатов

Replacement candidates — это похожие offer'ы, отобранные ML-моделью similarity:

  • Для accommodation: alternative properties в том же geo-кластере, той же property class, с похожим amenity-набором, в том же date range.
  • Для flights: alternative carriers, alternative time slots в day window ±4 часа, alternative routing (direct vs 1-stop), alternative cabin class (с явной маркировкой downgrade).
  • Для transfers: alternative supplier'ы, alternative vehicle classes, alternative pickup time в window ±30 минут.
  • Для activities: alternative time slots в день, alternative venues в том же geo-кластере, alternative activity types same theme.
  • Для insurance: alternative underwriter с похожим coverage scope.

Ranking

Каждый кандидат имеет similarity_score (0–1) и price_delta (relative to original). Ranking учитывает:

  • semantic similarity (similar amenities/features) — 50% weight;
  • price proximity (closer to original price = higher rank) — 30% weight;
  • supplier reliability (history of low drift) — 15% weight;
  • partner-specific preferences (если партнёр-tenant имеет saved preferences) — 5% weight.

В Stage 1-2 ML-модели нет — ranking использует rule-based similarity (point-counting по amenity match). На Stage 3+ переход на ML-модель.

Bundle integrity — каноничные правила связности

Bundle integrity проверяется при cascade analysis обязательно — это strong invariant тура.

1. Temporal connectivity (временная связность)

  • Flight → Transfer: между моментом landing и pickup transfer должно быть минимум 60 минут (обычно), 90 минут (для international с customs), 120 минут (если есть baggage delays).
  • Transfer → Hotel checkin: pickup transfer должен прибыть в check-in window отеля или иметь explicit late_arrival arrangement.
  • Activity → Activity в один день: минимум 30 минут transition time между activities.

2. Geographic connectivity (географическая связность)

  • Hotel → Activity: расстояние не должно превышать пороги, заданные partner-policy (по умолчанию 50 км в day-trip радиусе).
  • Multi-night accommodation: если тур включает несколько отелей в разных локациях, transfers между ними обязательны в bundle.

3. Capacity consistency

  • Все capacity-полa должны соответствовать party_size тура: hotel rooms ≥ party_size, flight seats = party_size, transfer seats ≥ party_size.

4. Date consistency

  • Insurance period должен покрывать весь trip dates — start no later than first booked date, end no earlier than last booked date.
  • Activities должны попадать в trip dates (не до flight arrival, не после departure).

При нарушении любого из этих правил после drift — bundle_integrity_break event. Если нарушение можно исправить через replacement — auto_replace срабатывает; если нет — требуется явное решение.

Auto-replace для cascade_noticeable — поток

cascade_noticeable выявлен

Выборка top-3 replacement candidates с наименьшим price_delta

Application каждого кандидата:
Шаг 1: temporary swap в копии тура
Шаг 2: пересчёт cascade с replacement
Шаг 3: проверка bundle integrity после swap
Шаг 4: классификация new severity

Если хотя бы один кандидат снижает severity до cascade_negligible:
→ выбирается best (highest similarity_score)
→ автоматическая замена с публикацией DraftItemReplacedEvent
→ партнёр получает notification с ДО/ПОСЛЕ компарацией
→ партнёр имеет 24 часа для отката замены через explicit reject action

Если ни один кандидат не помогает:
→ escalation до cascade_material (требуется ручное решение)

Manual review для cascade_material

При cascade_material:

  • Никакая автоматическая замена не выполняется — слишком велика вероятность что партнёр имеет специфические preferences, которые ML/rules не учли.
  • Партнёр получает notification с полной информацией:
    • Что изменилось (item, scope изменения).
    • Полный новый total_price и delta vs original.
    • Top-5 ranked replacement candidates с обоснованием.
    • Опции: «принять текущий drift с новой ценой», «выбрать кандидата», «полностью отменить тур», «связаться с support».
  • Тур переходит в awaiting_partner_decision substate; commit в priced/proposed заблокирован.
  • Если партнёр не отвечает в течение tenant_configurable_window (обычно 24-48 часов) — тур автоматически переходит в expired с возвратом всех зарезервированных средств.

Атомарность auto-replace для bundle integrity

Когда replacement requires множественной замены (например, замена hotel A автоматически требует обновления transfer'ов и activities привязанных к hotel A's геолокации) — все замены выполняются атомарно через Saga:

  • Либо все компоненты успешно заменены и тур пересчитан;
  • Либо ничего не меняется, и тур переходит в cascade_material для manual review.

Частичная замена (заменили hotel но не updated transfer) запрещена — это создаёт broken bundle.

Метрики качества cascade handling

Для мониторинга:

  • cascade_event_rate — DriftEvent'ов, перешедших в cascade analysis (обычно 100%, но если ниже — это bug).
  • auto_replace_success_rate — доля cascade_noticeable, успешно auto-replaced без partner intervention.
  • auto_replace_revert_rate — доля auto-replacements, которые партнёр потом отменил (показатель качества similarity model).
  • cascade_material_rate — доля cascade events, переходящих в material (показатель supplier reliability).
  • bundle_integrity_break_rate — частота нарушений bundle integrity (показатель quality replacement candidates).

При auto_replace_revert_rate > 20% — review similarity ranking model или ослабление threshold для auto_replace.

Связь с экономической моделью партнёра

Auto-replace может изменить commission баланс партнёра:

  • Если replacement дешевле original → партнёрская commission меньше → loss-of-commission compensation policy (partner-tier-зависимая).
  • Если replacement дороже original → партнёрская commission больше, но превышение должно быть явно одобрено партнёром (auto-replace не повышает total price без явного consent).

Каноничное правило: auto_replace не увеличивает total_price тура без явного consent партнёра. Если все top-3 candidates дороже original — это автоматически escalation в cascade_material.

Обоснование (тезисы)

Тезис 1. Drift одного элемента тура — это сигнал к пересмотру всего тура.

Альтернативы: (а) обрабатывать каждый item drift независимо; (б) каскадный анализ при любом drift.

Trade-off: вариант (а) допускает ситуацию «партнёр получил уведомление об одном drift, согласился, а потом обнаружил что весь тур стал невыгодным или нарушена связность»; вариант (б) — каноничный, обеспечивает целостность опыта партнёра.

Тезис 2. Bundle integrity — strong invariant, не optional check.

Альтернативы: (а) проверять integrity только в момент booking commit; (б) проверять integrity при каждом drift event.

Trade-off: вариант (а) откладывает проблему до checkout, что приводит к потере конверсии; вариант (б) — каноничный, проблемы выявляются и решаются заблаговременно.

Тезис 3. Auto-replace ограничен strict guardrails.

Альтернативы: (а) auto-replace для всех cascade severities включая material; (б) auto-replace только для cascade_noticeable, material — manual; (в) никакого auto-replace.

Trade-off: вариант (а) приводит к недовольству партнёров «вы заменили мой тур без спроса»; вариант (в) теряет UX-преимущество и нагружает партнёра ручными решениями для тривиальных случаев; вариант (б) — каноничный баланс.

Тезис 4. Replacement candidates ранжируются ML-моделью, но fallback на rule-based ranking гарантирован с Stage 1.

Альтернативы: (а) auto-replace доступен только с Stage 3 после ML-активации; (б) Stage 1 — rule-based ranking, Stage 3 — переход на ML.

Trade-off: вариант (а) откладывает значимую UX-фичу на 2 года; вариант (б) — каноничный rolling rollout.

Атомарность бронирования пакетного тура

Проблема

Пакетный тур из 3 компонентов = 3 отдельных бронирования у потенциально 3 разных поставщиков. При попытке подтверждения один поставщик может ответить успехом, второй — отказом, третий — задержкой.

Это классическая проблема распределённой транзакции — требуется атомарность «всё или ничего».

Решение: паттерн саги (saga pattern)

Платформа использует саги с компенсирующими действиями (compensating transactions):

TourBookingTransaction.start

Step 1: lock_payment — блокирует платёжное намерение клиента (через PaymentIntent)

├── failure → finish: payment_locked_failed

Step 2: confirm_component_1 — отправка бронирования первому поставщику

├── failure → compensate: unlock_payment, finish: failed

Step 3: confirm_component_2 — отправка бронирования второму поставщику

├── failure → compensate: cancel_component_1, unlock_payment, finish: rolled_back

Step 4: confirm_component_3 — отправка бронирования третьему поставщику

├── failure → compensate: cancel_component_1, cancel_component_2, unlock_payment, finish: rolled_back

Step 5: capture_payment — фиксация платежа клиента

├── failure → compensate: cancel_all_components, finish: payment_failure

Step 6: finish: confirmed

Замок платежа перед компонентами

Ключевое решение: платежное намерение блокируется (authorized, но не captured) до попытки бронирования компонентов. Это:

  • Гарантирует, что у клиента есть средства до начала операции.
  • Позволяет откат платежа без возврата (просто void) при провале.
  • Минимизирует время, в которое деньги клиента «висят» в платёжной системе.

См. Платёжный домен для деталей PaymentIntent.authorized и captured.

Компенсирующие действия

При провале на любом шаге выполняются компенсирующие транзакции в обратном порядке:

  • Каждый успешный Booking отменяется через стандартный поток отмены поставщика.
  • PaymentIntent переводится в voided или refund_pending в зависимости от состояния.
  • Клиент уведомляется о провале с разъяснением.

Журнал саги

TourBookingTransaction.saga_log хранит полный журнал всех шагов и компенсаций для:

Обработка частичных задержек

Поставщики иногда отвечают с задержкой (pending_supplier_confirmation, см. API Contracts, состояния бронирования). Сага не блокируется ожиданием на каждом компоненте — переходит в асинхронный режим:

  • Шаги с быстрыми ответами завершаются сразу.
  • Шаги с задержкой переводятся в awaiting_supplier.
  • Сага продолжает обработку других шагов параллельно.
  • При окончательном ответе поставщика — продолжение или компенсация.

Это требует более сложной саги, но даёт значительно лучший пользовательский опыт при работе с поставщиками с разной скоростью.

Партнёрская модель интеграции

Программный интерфейс конструктора туров (Tour Builder API)

Партнёрский интерфейс — набор примитивных операций, не одна «магическая» функция:

ОперацияНазначениеТариф
create_draftСоздать пустой черновикВсе с Tour Builder access
add_itemДобавить компонентВсе с Tour Builder access
replace_item_variantЗаменить вариант компонентаВсе с Tour Builder access
remove_itemУдалить компонентВсе с Tour Builder access
validate_compositionПроверить правила композицииВсе
list_violationsПолучить список нарушенийВсе
request_pricingЗапросить цены (создать Quote'ы)Все
generate_proposalСгенерировать документ предложенияВсе
commit_tourПодтвердить тур (запустить TourBookingTransaction)Все
cancel_tourОтменить тур после подтвержденияВсе
query_driftПолучить список текущих drift eventsВсе
resolve_driftПринять решение по drift eventВсе
apply_templateПрименить шаблон пакетного тураВсе с шаблонной поддержкой
clone_draftСкопировать черновикProfessional+
export_artifactЭкспортировать артефакт (PDF)Professional+
bulk_pricingЗапросить цены для множества вариантов одновременноEnterprise

Каждая операция — отдельный эндпоинт с явным контрактом. Это даёт партнёру полный контроль над процессом.

Webhooks для async-уведомлений

Партнёр подписывается на события Tour Builder:

  • tour.draft.violation_detected — обнаружено нарушение правила.
  • tour.drift.detected — обнаружен drift одного из компонентов.
  • tour.booking.committing_started — начало TourBookingTransaction.
  • tour.booking.confirmed — успешное подтверждение.
  • tour.booking.rolled_back — откат при провале.

Все webhooks следуют общему механизму из Уведомления и коммуникации.

Стабильность контракта

Программный интерфейс Tour Builder следует общим правилам стабильности из Программный интерфейс как продукт:

  • Минимум 12 месяцев гарантированной поддержки версии.
  • Запрет на ломающие изменения внутри версии.
  • Контрактное тестирование на каждом релизе.

Ограничение скорости

Tour Builder — дорогостоящий контур: каждая операция композиции потенциально обращается к множеству поставщиков, ML-моделей, проверок правил. Применяется отдельное ограничение скорости для Tour Builder API, более строгое чем для базового партнёрского интерфейса:

  • Максимум N черновиков в активном состоянии на тенанта одновременно.
  • Максимум M вызовов validate_composition в минуту.
  • Максимум K параллельных commit_tour транзакций.

Это защищает платформу от злоупотреблений и обеспечивает справедливое распределение ресурсов.

Учёт потребления

Метрика tour_builder_operations (см. Учёт потребления и квоты) — основная метрика тарификации Tour Builder доступа. Каждая значимая операция (commit_tour, request_pricing, generate_proposal) учитывается отдельно.

Ценообразование пакетного тура

Каноничная формула

Стоимость тура для клиента:

tour_price =
Σ (component_quote.price × applicable_fx_rate)
+ platform_package_margin
+ applicable_taxes (per-jurisdiction, через TOMS для пакетных туров в EU)
- applied_discounts (промо, скидка партнёра, скидка за объём)

Маржа платформы на пакетных турах

Платформа применяет отдельную маржу для пакетных туров (platform_package_margin), отличную от индивидуальных бронирований. Обоснование:

  • Tour Builder — дополнительная ценность (агрегация, проверка совместимости, атомарное бронирование, drift handling).
  • Эту ценность платформа монетизирует через маржу.
  • Размер маржи зависит от тарифа партнёра (для Enterprise — может быть нулевой или revenue share моделью).

Налоговый режим

Пакетный тур в EU подпадает под маржинальный режим Tour Operators (TOMS) — платформа платит НДС только с маржи, не с полной стоимости. Это существенно влияет на структуру цены и требует:

  • Явного разделения cost-of-goods-sold и маржи в финансовом учёте.
  • Корректного оформления чеков и счетов.
  • Соответствующей конфигурации провайдера платёжных услуг.

Подробности — в Соответствие требованиям регуляторов.

Согласованность валют

Все компоненты тура должны быть в одной валюте отображения для клиента, даже если поставщики выставляют счета в разных валютах. Конвертация — через FxRateSnapshot (см. Интернационализация и локализация).

Метрики и наблюдаемость

Каноничные метрики операционной модели

  • Время от создания черновика до подтверждения тура — медианное и 95-й процентиль. Позволяет обнаружить деградацию пользовательского опыта.
  • Доля черновиков, доходящих до подтверждения (conversion rate) — главная продуктовая метрика.
  • Доля провалов TourBookingTransaction — операционная метрика. Цель — менее 2%.
  • Среднее число drift events на черновик — индикатор актуальности данных платформы.
  • Доля автоматически разрешённых drift events vs ручных — характеристика тарифа.
  • Среднее число элементов в подтверждённом туре — индикатор сложности использования.
  • Распределение типов туров (linear/themed/package) — продуктовый сигнал.
  • Среднее время выполнения саги TourBookingTransaction — операционный показатель, влияющий на пользовательский опыт.

Алерты

Через Аналитика и бизнес-аналитика и Наблюдаемость и реагирование на инциденты:

  • Алерт при превышении 5% провалов TourBookingTransaction за час — оперативная команда расследует.
  • Алерт при появлении новых видов нарушений CompositionRule — продуктовая команда оценивает.
  • Алерт при росте material drift events выше нормального уровня — анализ источников (поставщик деградирует?).

События операционной модели Tour Builder

СобытиеКогдаГлавные потребители
tour.draft.createdСоздание черновикаПлатформа данных, партнёрская консоль
tour.draft.item_addedДобавление элементаКонтур наблюдаемости, drift tracker
tour.draft.item_replacedЗамена вариантаКонтур наблюдаемости
tour.draft.violation_detectedОбнаружение нарушенияПартнёр (через webhook), оперативная команда
tour.draft.violation_resolvedРазрешение нарушенияПлатформа данных
tour.drift.detectedОбнаружение driftПартнёр (через webhook), пользователь
tour.drift.resolvedРазрешение driftПлатформа данных
tour.proposal.generatedГенерация предложенияКонтур уведомлений, аналитика
tour.booking.transaction_startedНачало TourBookingTransactionПлатёжный контур, наблюдаемость
tour.booking.saga_step_completedШаг сагиНаблюдаемость, аудит
tour.booking.confirmedУспешное подтверждениеВсе потребители
tour.booking.rolled_backОткатКонтур уведомлений, оперативная команда
tour.booking.partial_failureЧастичный провал требующий вниманияСрочный алерт оперативной команде

Полная таксономия — в Первоначальная таксономия событий.

Регуляторные следствия

Директива о пакетных турах (EU 2015/2302, Package Travel Directive)

Применима ко всем пакетным турам, продаваемым в EU. Главные обязательства:

  • Преддоговорная информация — клиент получает обязательный набор информации о туре до подписания.
  • Финансовая гарантия — продавец (платформа или тенант) обязан иметь обеспечение на случай неплатёжеспособности.
  • Право клиента на возврат при существенных изменениях (material drift в нашей модели — основание для права возврата).
  • Ответственность продавца за исполнение всех компонентов тура — клиент обращается только к продавцу, не к каждому поставщику.

Модель отвечающего за платежи продавца определяет, кто именно несёт эти обязательства — платформа (модель А) или тенант (модель Б). Подробности — в Соответствие требованиям регуляторов.

Маржинальный режим НДС (TOMS)

В EU для пакетных туров применяется маржинальный режим. Каноничная архитектура платформы поддерживает это через:

  • Явное разделение cost_of_goods_sold и platform_package_margin в TourBookingTransaction.
  • Корректные форматы счетов (с указанием маржинального режима).
  • Журнал для НДС-отчётности.

Юрисдикционные особенности

Не-EU юрисдикции имеют свои правила:

  • Украина — национальное законодательство о туристическом продукте, лицензирование туроператоров.
  • Казахстан — национальные правила.

Каждое расширение географии — добавление юрисдикционной модели в реестр.

Фазы развёртывания

Фаза Bootstrap (0–6 месяцев)

Что разворачивается:

  • Каноничная модель (CompositionRule, CompositionViolation, DriftEvent, TourBookingTransaction) зафиксирована в схеме хранения.
  • Базовый прототип потока создания черновика на одном типе тура (linear).
  • Простые правила композиции (temporal_consistency, capacity_consistency).
  • Прототип атомарного подтверждения для тура из 2 компонентов.

Триггер выхода:

  • Готовность к запуску партнёрского доступа Tour Builder в production.

Фаза 2 — Production Tour Builder (6–18 месяцев)

Что разворачивается:

  • Полный программный интерфейс Tour Builder для партнёров с Tour Builder access.
  • Все 3 типа туров (linear, themed, package).
  • Полный набор CompositionRule для каждого типа.
  • Полный drift handling с уведомлениями.
  • Атомарное подтверждение через сагу для туров до 5 компонентов.
  • Webhooks для всех событий Tour Builder.
  • Партнёрская консоль для управления черновиками.
  • Правила маржинального режима (TOMS) в EU.

Триггер выхода:

  • Появление спроса на туры с большим числом компонентов (более 5).
  • Появление partner-specific правил композиции.

Фаза 3 — Smart Tour Builder (18–30 месяцев)

Что разворачивается:

  • ML-модели похожести для автоматических замен при drift.
  • ML-модели рекомендаций (что добавить к туру).
  • Шаблоны пакетных туров с динамической генерацией под параметры.
  • Атомарное подтверждение для туров с большим числом компонентов через расширенную сагу.
  • Расширенная аналитика операционной модели для партнёров.
  • Tenant-specific CompositionRule для корпоративных партнёров.

Фаза 4 — Maturity (30+ месяцев)

Что разворачивается:

  • Conversational интерфейс конструктора туров (LLM-based).
  • Multi-region обработка с локализацией данных.
  • Маркетплейс шаблонов туров (партнёры могут публиковать шаблоны).
  • ML-driven динамическое ценообразование пакетов.

Архитектурные решения с тезисным обоснованием

Решение 1. Raw API constructor по модулям, не «магическая функция»

Цель: дать партнёру максимальный контроль и обеспечить стабильность контракта.

Тезисы:

  1. «Магическая» функция (например, build_tour(parameters)) скрывает логику и затрудняет отладку. Партнёр не знает, почему получился именно такой тур.
  2. Примитивные операции дают партнёру полный контроль и прозрачность.
  3. Стабильность контракта проще обеспечить для набора простых операций, чем для одной сложной.
  4. Современные платформы (Stripe Connect, Twilio Studio) — все строят сложные потоки из примитивных операций. Это база.

Решение 2. CompositionRule как отдельная сущность

Цель: обеспечить расширяемость правил без релизов кода.

Тезисы:

  1. Правила композиции эволюционируют по мере роста платформы и требований партнёров. Жёсткое зашивание в код блокирует развитие.
  2. Tenant-specific правила (для корпоративных партнёров) невозможны без отдельной сущности.
  3. Аудит правил для регулятора — единственный способ через структурированную модель.

Решение 3. Drift handling как first-class

Цель: обеспечить честность с пользователем и соответствие регуляторам.

Тезисы:

  1. Молчаливое использование устаревших данных — нарушение Директивы о пакетных турах (которая требует уведомлять клиента о существенных изменениях).
  2. Без структурированной модели DriftEvent невозможно доказать регулятору, что пользователь был уведомлён.
  3. Прозрачность повышает доверие партнёров и пользователей.

Решение 4. Атомарность через сагу с компенсирующими действиями

Цель: обеспечить «всё или ничего» для пакетного тура.

Тезисы:

  1. Распределённые транзакции невозможны через несколько внешних поставщиков. Сага — единственный практический паттерн.
  2. Замок платежа перед компонентами минимизирует финансовый риск для платформы и клиента.
  3. Журнал саги обеспечивает replay-safety и аудит.
  4. Современные платформы (Uber, Netflix) — все строят критичные потоки на сагах.

Решение 5. Отдельное ограничение скорости и тарифика для Tour Builder

Цель: защита от злоупотребления дорогостоящим контуром и справедливое распределение.

Тезисы:

  1. Tour Builder — самый дорогой партнёрский контур (множество вызовов поставщикам, ML, проверок).
  2. Без отдельных ограничений недобросовестный партнёр может монополизировать ресурсы.
  3. Отдельная тарификация даёт справедливую модель — кто использует, тот и платит.

Открытые развилки

Развилка 1. Глубина ML-driven автоматических замен

В фазе 3 — ML-driven автозамены при material drift. Глубина (от предложения вариантов до полностью автоматической подмены без подтверждения) — открытая развилка.

Эскалируется: при создании ML-стратегии в фазе 3.

Развилка 2. Поддержка туров с большим числом компонентов (более 10)

Сложные корпоративные туры могут содержать 10+ компонентов. Атомарность через сагу для такого числа компонентов сложна.

Эскалируется: при появлении корпоративных партнёров с такими требованиями.

Развилка 3. Маркетплейс шаблонов туров

В фазе 4 — возможность партнёрам публиковать собственные шаблоны на платформе с разделением выручки. Существенная коммерческая возможность, требующая отдельной коммерческой модели.

Эскалируется: при стратегическом пересмотре в фазе 4.

Развилка 4. Conversational интерфейс Tour Builder

Через LLM — возможность строить тур через диалог («хочу романтический weekend в Праге для двоих в июне»). В фазе 4.

Эскалируется: при создании conversational-стратегии в Платформа машинного обучения.

Развилка 5. Поддержка туров с динамическими ценами от поставщиков

Некоторые поставщики (например, авиабилеты) имеют высоковолатильные цены, меняющиеся минута к минуте. Текущая модель Quote с фиксированной ценой может не подойти. Требуется отдельная стратегия — quote-on-commit или специальные high-volatility варианты.

Эскалируется: при добавлении поставщиков авиабилетов или подобных.

Связанная документация

Корневые архитектурные документы

Связанные доменные документы

Документы развития

Операционная сторона

Архитектурные правила