Event Contract: pin.crm.booking-confirmed (BookingConfirmedEvent)
Версионируемый контракт интеграционного события «бронь подтверждена». Consumer-документы ссылаются сюда, не
переописывая payload. Дом полей — entity: BookingEntity (раздел «Доменные и
интеграционные события», строка BookingConfirmedEvent); здесь перечислено только то, что реально в событии.
Назначение
Фиксирует переход брони в состояние Booked — объект окончательно закреплён за сделкой по программе
бронирования застройщика (booking-program.md). Публичное: даёт downstream
snapshot без синхронного запроса в crm, чтобы chessboard перевёл объект недвижимости в статус «Бронь» и завёл таймер
брони (BookingExpiresAt), не читая таблицы CRM (границы контекстов —
service-boundaries.md). Смену статуса объекта в шахматке
выполняет консюмер chessboard; CRM чужие таблицы не пишет.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.booking-confirmed |
| Topic | pin.crm.booking-confirmed.v1 |
| Partitions / key | 6 (ADR-0056, 1–10k активных) / key = AggregateId = BookingId — per-booking порядок |
| ContractVersion | 1 |
| Producer | crm: подтверждение брони (переход → Booked, booking.md INV-BKG-3/INV-BKG-4) |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата: пока переход не зафиксирован, событие не отправляется;
после фиксации — доставляется гарантированно (at-least-once), ключ = BookingId.
Payload BookingConfirmedEvent
Строго типизированный snapshot момента подтверждения. Имена — реальные поля BookingEntity
(booking.md); в скобках — семантический ярлык постановки. BookingExpiresAt и
BookedAt в агрегате nullable (DateTimeOffset?), но на переходе → Booked оба заполнены, поэтому в
payload обязательны.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
BookingId | Guid | Да | BookingEntity.Id | Immutable | Ключ агрегата (= AggregateId, Kafka key). |
DealId | Guid | Да | BookingEntity.DealId | Immutable | Сделка-владелец процесса (deal.md). |
TenantId | Guid | Да | BookingEntity.TenantId | Immutable | Застройщик объекта (DeveloperCompanyId), tenant boundary. |
PropertyObjectId | Guid | Да | BookingEntity.PropertyObjectId | Immutable | Объект шахматки; chessboard ставит статус «Бронь». |
ProgramId | Guid | Да | BookingEntity.ProgramId | Immutable | Программа бронирования (BookingProgramId); условия — иммутабельный snapshot в брони. |
BookingExpiresAt | DateTimeOffset | Да | BookingEntity.BookingExpiresAt | Additive only | Дедлайн фазы «Бронь» = BookedAt + ProgramTerms.BookingDurationHours (таймер «BookingUntil»); chessboard заводит таймер до этого момента. |
BookedAt | DateTimeOffset | Да | BookingEntity.BookedAt | Immutable | Момент подтверждения брони (→ Booked, «ConfirmedAt»). |
Payload НЕ несёт условий программы (уже разосланы snapshot-ом в
BookingCreatedEvent), сумм/оплаты (см. BookingPaidEvent),
реквизитов (RequisiteSetId — только логическая ссылка на profile) и ФИО/телефонов клиента.
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Бесплатная программа (ProgramTerms.IsPaid = false): подтверждение брони до ReserveExpiresAt (INV-BKG-3) | Reserved → Booked | атомарно со сменой состояния агрегата |
Платная программа: подтверждение оплаты по чеку менеджером застройщика + подтверждение брони при PaymentStatus = Paid (INV-BKG-3/4) | AwaitingPayment → Booked | та же |
На переходе фиксируются BookedAt = now и BookingExpiresAt = BookedAt + ProgramTerms.BookingDurationHours,
а также доменное событие BookingConfirmed для таймлайна сделки; смена состояния и публикация
интеграционного BookingConfirmedEvent происходят в одном согласованном изменении.
Гонка «истечение по таймеру / подтверждение менеджером» разрешается оптимистичной блокировкой по версии — проигравший повторяет операцию с перечиткой, поэтому событие публикуется ровно для победившего перехода.
Потребители (consumers)
Сверено с booking.md (BookingConfirmedEvent, консюмеры). Головной потребитель —
chessboard.
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
chessboard | Объект недвижимости PropertyObjectId → статус «Бронь» + таймер до BookingExpiresAt (проекция) | MessageId + first-seen BookingId | < 1 мин |
catalog | Обновление статуса объекта недвижимости в витрине (проекция шахматки) | MessageId | < 5 мин |
notifications | Уведомление участникам сделки «бронь подтверждена» | MessageId | < 5 мин |
Все консюмеры идемпотентны по MessageId (повторная доставка Kafka не даёт двойного эффекта); статус объекта недвижимости —
идемпотентный upsert проекции.
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию суффиксом.v1по конвенции.- Additive-only для minor: новые поля — как optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа/смысла) — новый
ContractVersion = 2+ топикpin.crm.booking-confirmed.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Enum в payload нет (только
Guid/DateTimeOffset) — unknown-enum правило неприменимо. - Порядок per-booking гарантирован key-ом (
BookingId); лайфцикл объекта недвижимости между разными топиками брони (booking-created/-confirmed/-expired/-cancelled) консюмер упорядочивает по бизнес-состоянию, а не по кросс-топик offset.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
BookingId, DealId, TenantId, PropertyObjectId, ProgramId | Internal | include |
BookingExpiresAt, BookedAt | Internal | include |
Событие содержит только идентификаторы и метки времени — PII (ФИО/телефоны клиента) в payload
отсутствует, поэтому маскирование полей не требуется; ФИО/телефон живут в identity/profile и в
логах producer/consumer маскируются по общему правилу (+7***1234, инициалы). Событие НЕ содержит secrets,
tokens, credentials, платёжных реквизитов, эскроу-счетов и unbounded вложений. Tenant boundary передан
явным TenantId; доступ к топику ограничен m2m ACL Kafka (chessboard/catalog/notifications).
Доставка, retry/DLQ и восстановление
- Гарантия. At-least-once после атомарной публикации; ключ =
BookingId— per-booking порядок сохранён. Дубликаты возможны — консюмеры идемпотентны. - Идемпотентность.
chessboard—MessageId+ first-seenBookingId(статус «Бронь» — идемпотентный upsert проекции);catalog/notifications— поMessageId. - Retry. Транзиентный сбой консюмера → повтор с backoff без продвижения оффсета этой брони; «уже применено» (объект недвижимости уже «Бронь») — no-op, не ошибка.
- DLQ. Неразбираемое/несовместимое сообщение после конечного числа попыток → DLQ + алерт; другие брони не блокируются.
- Кросс-топик порядок.
booking-confirmedможет обогнать или отстать от других фаз брони в своих топиках;chessboardупорядочивает лайфцикл объекта недвижимости по бизнес-состоянию: если бронь уже освобождена/отменена (обработаны-expired/-cancelled) — «Бронь» не выставляется (fail-safe). Отсутствие ранее виденногоbooking-createdдля этого объекта недвижимости не блокирует обработку — целевой статус «Бронь» самодостаточен (snapshot условий не нужен, он был вbooking-created). - Промежуточное состояние. Таймлайн-строка
BookingConfirmedиBookingExpiresAtвидны в карточке сделки сразу (данныеcrm); смена цвета объекта недвижимости в шахматке и уведомление — с задержкой (< 1–5 мин). - Гонка «истечение ↔ подтверждение». Разрешена на источнике оптимистической блокировкой (проигравший
перечитывает состояние), поэтому
BookingConfirmedEventпубликуется ровно для победившего перехода — консюмер не получает противоречивую пару «confirmed для уже истёкшей брони».
Acceptance-критерии (контракт потребления)
- chessboard — бронь. Дано: опубликован
BookingConfirmedEvent. Когдаchessboardобрабатывает. Тогда объект недвижимости → «Бронь» + таймер доBookingExpiresAt. - Идемпотентность. Дано: событие доставлено дважды. Когда обрабатывается повторно. Тогда статус объекта недвижимости не меняется повторно.
- Устаревшее событие. Дано: для этой брони уже обработана отмена/истечение. Когда приходит запоздавший
booking-confirmed. Тогда «Бронь» не ставится (fail-safe). - catalog/notifications. Дано: событие опубликовано. Когда обрабатывают консюмеры. Тогда витрина обновляет статус объекта недвижимости, участники сделки получают уведомление «бронь подтверждена».
- Несовместимый payload. Дано: сообщение без обязательного поля. Когда повтор не помог. Тогда DLQ + алерт.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (6): GET /api/developer/crm/booking-programs — Программы бронирования, GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), POST /api/agent/crm/bookings/{bookingId}/confirm — Подтвердить бронь (бесплатная программа), POST /api/developer/crm/bookings/{bookingId}/confirm-payment — Подтвердить оплату брони, POST /api/developer/crm/bookings/{bookingId}/extend — Продлить бронь, PUT /api/investor/crm/bookings/{bookingId}/requisites — Выбрать реквизиты для брони
- Use Cases (4): UC-BKG-002. Бронирование Резерв → Бронь, UC-BKG-003. Истечение резерва/брони и продление, UC-BKG-004. Оплата платного бронирования (чек + подтверждение), UC-CAL-007. Дедлайны брони CRM в календаре агента
- События (4): Event Contract: pin.crm.booking-cancelled (BookingCancelledEvent), Event Contract: pin.crm.booking-created (BookingCreatedEvent), Event Contract: pin.crm.booking-expired (BookingExpiredEvent), Event Contract: pin.crm.booking-paid (BookingPaidEvent)
- Консюмеры (1): Consumer: Calendar CRM Booking Sync (calendar-crm-booking-events)
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- scenarios (1): UC-OVR-003. Бронирование объекта у агента (+3a)
- index.md (1): Calendar Service
- registries (1): Реестр событий PIN
Связанные документы
- Дом агрегата: booking.md (переходы
→ Booked, INV-BKG-3/4/6); условия и таймеры: booking-program.md; родительский процесс: deal.md. - Соседние контракты топиков брони: booking-created.md, booking-paid.md, booking-expired.md, booking-cancelled.md; карта событий — ../README.md.
- Консюмер статуса объекта недвижимости —
docs/06-services/chessboard/(будущий путь по конвенции).