Event Contract: pin.crm.booking-created (BookingCreatedEvent)
Версионируемый контракт интеграционного события «Бронирование создано (Резерв)». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей агрегата — entity: BookingEntity;
условия программы — booking-program.md.
Назначение
Фиксирует старт процесса бронирования: у сделки создан агрегат BookingEntity в статусе Reserved, объект
временно закрепляется за клиентом на ProgramTerms.ReserveDurationMinutes (по умолчанию 1 час). Публичное:
даёт downstream-контекстам факт и дедлайн резерва без синхронного запроса в crm — chessboard переводит
объект недвижимости в статус «Резерв» и ставит таймер (границы контекстов: CRM чужие таблицы не пишет,
service-boundaries), messenger рисует карточку брони в чате
сделки (ADR-0055).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.booking-created |
| Topic | pin.crm.booking-created.v1 |
| Partitions / key | 3 (ADR-0056, 1–10k активных броней) / key = AggregateId = BookingId (порядок событий одной брони) |
| ContractVersion | 1 |
| Producer | crm: операция create-booking (старт брони; InitiatedBy = инвестор с витрины / агент из сделки / Менеджер ПИН) |
| Статус | draft |
Событие публикуется атомарно с созданием агрегата и переводом сделки в Booking: пока изменение не
зафиксировано, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ =
BookingId.
Payload BookingCreatedEvent
Дом полей — entity: BookingEntity (раздел «Доменные и интеграционные события»);
здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
BookingId | Guid | Да | BookingEntity.Id | Immutable | Ключ агрегата (= Kafka key, = AggregateId). |
TenantId | Guid | Да | BookingEntity.TenantId | Immutable | Застройщик объекта (DeveloperCompanyId, ADR-0052). |
DealId | Guid | Да | BookingEntity.DealId | Immutable | Сделка-владелец процесса (deal.md). |
PropertyObjectId | Guid | Да | BookingEntity.PropertyObjectId | Immutable | Объект в шахматке (chessboard); по нему выставляется статус «Резерв». |
InitiatedBy | string (enum BookingInitiator) | Да | BookingEntity.InitiatedBy | Additive only (новые значения) | Точка входа: Investor / Agent / PinManager. |
ReserveExpiresAt | DateTimeOffset | Да | BookingEntity.ReserveExpiresAt | Immutable | Дедлайн резерва (CreatedAt + ProgramTerms.ReserveDurationMinutes); chessboard ставит таймер по нему. |
ProgramTerms | BookingProgramTermsSnapshot | Да | BookingEntity.ProgramTerms | Additive only | Иммутабельный снимок условий программы бронирования на момент создания. |
CreatedAt | DateTimeOffset | Да | BookingEntity.CreatedAt | Immutable | Момент старта резерва (= создание заявки на бронирование). |
Вложенная модель BookingProgramTermsSnapshot
Snapshot условий; дом полей — booking.md (VO BookingProgramTerms) →
booking-program.md. RefundPolicy в событие не входит (нужен только UI CRM,
читается синхронно).
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
Name | string | Да | ≤ 128 | Название программы («Бесплатная бронь 4 часа», «Платная бронь 10 дней»). |
IsPaid | bool | Да | — | Платная ли бронь (US-INV-06/21). |
PriceAmount | decimal? | Нет (обязателен при IsPaid) | numeric(18,2) > 0 | Стоимость бронирования. |
PriceCurrencyId | short | Да | ссылка на справочник валют handbook; RUB-only (ADR-0048) | Валюта стоимости бронирования. |
PriceCurrency | CurrencySnapshot | Да | поля — snapshots.md, не переописываются | Display-снимок валюты на момент публикации (код/имя/знак). |
ReserveDurationMinutes | int | Да | > 0 (default 60) | Длительность фазы «Резерв» — по ней вычислен ReserveExpiresAt. |
BookingDurationHours | int | Да | > 0 | Длительность будущей фазы «Бронь» (после подтверждения). |
Payload НЕ содержит ФИО/телефон клиента, RequisiteSetId, суммы оплаты по чеку — только факт и условия
резерва. Персональные данные остаются в identity/lead.md; реквизиты — в profile.
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Операция create-booking: сделка Status ∈ {Application, Booking} (INV-BKG-2), объект недвижимости свободен (INV-BKG-001), выбрана программа | [*] → Reserved (агрегат); Deal.OnBookingCreated() (Application → Booking) | атомарно со сменой состояния агрегата и сделки |
Последующие переходы брони публикуются отдельными событиями того же семейства: подтверждение — booking-confirmed.md, оплата — booking-paid.md, истечение — booking-expired.md, отмена — booking-cancelled.md.
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
chessboard | Объект недвижимости PropertyObjectId → статус «Резерв» + таймер до ReserveExpiresAt (проекция статуса объекта; CRM чужие таблицы не пишет) | MessageId + per-property-object guard | < 1 мин (визуальная блокировка объекта недвижимости) |
messenger | Карточка «бронирование создано (Резерв до ReserveExpiresAt)» в чат сделки DealId (ADR-0055) | MessageId | < 5 мин |
Оба консюмера идемпотентны по MessageId: повторная доставка не создаёт дублей проекции/карточки.
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом.v1.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.crm.booking-created.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum
InitiatedBy— fallback: сохранить строку как есть, warning; проекция статуса объекта недвижимости от значенияInitiatedByне зависит (fail safe).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
BookingId, TenantId, DealId, PropertyObjectId, InitiatedBy, ReserveExpiresAt, CreatedAt | Internal | include |
ProgramTerms.* (Name/IsPaid/PriceAmount/PriceCurrencyId/PriceCurrency/ReserveDurationMinutes/BookingDurationHours) | Internal | include (бизнес-условия программы, не платёжные реквизиты) |
- PII (ФИО/телефоны) в событии отсутствует по дизайну — она не покидает
identity/lead; консюмеры, которым нужно отобразить клиента, берут его из своей репликиpin.identity.users.v1. Если ФИО/телефон всё же попадёт в логи producer/consumer при отладке — маскирование обязательно (+7***1234, инициалы). - Доступ к топику — m2m ACL Kafka (
chessboard/messenger/notifications). - Событие НЕ содержит secrets, tokens, raw credentials, документов, платёжных данных по чеку.
Доставка, retry/DLQ и восстановление
- Гарантия. At-least-once после атомарной публикации из outbox; ключ =
BookingId— порядок событий одной брони в топике сохранён. Дубликаты возможны (переотправка) — консюмеры обязаны быть идемпотентны. - Идемпотентность консюмеров.
chessboard— поMessageId+ per-property-object guard (повторная доставка не переставляет статус);messenger— поMessageId(карточка брони в чат не дублируется). - Retry. Транзиентный сбой консюмера (недоступность своей БД/зависимости) → повтор с backoff, порядок по ключу сохраняется (партиция не продвигается дальше застрявшего оффсета одной брони). Бизнес-«уже применено» (объект недвижимости уже «Резерв» для этой брони) — не ошибка, а no-op.
- DLQ. Неразбираемое/несовместимое сообщение (ошибка десериализации, отсутствуют обязательные поля) после конечного числа попыток уходит в DLQ с алертом; лайв-сообщения других броней не блокируются.
Между booking-created и более поздними booking-confirmed/-expired/-cancelled одной брони межтопиковый
порядок не гарантирован; chessboard применяет статус по бизнес-состоянию: если для этого BookingId
уже обработана более поздняя фаза (объект недвижимости освобождён/«Бронь»), устаревший Резерв из
booking-created не применяется (fail-safe, warning в лог).
До обработки события витрина/шахматка показывают объект недвижимости «Свободен» (< 1 мин); двойного
бронирования это не создаёт — единственность держит источник crm (INV-BKG-001), а не проекция шахматки.
Acceptance-критерии (контракт потребления)
- chessboard — резерв. Дано: опубликован
BookingCreatedEvent, объект недвижимости свободен. Когдаchessboardобрабатывает событие. Тогда объект недвижимости → «Резерв» + таймер доReserveExpiresAt. - chessboard — идемпотентность. Дано: событие доставлено дважды. Когда обрабатывается повторно. Тогда статус объекта недвижимости не меняется повторно (no-op).
- chessboard — устаревшее событие. Дано: для этой брони уже обработана отмена/истечение. Когда приходит
запоздавший
booking-created. Тогда «Резерв» не ставится (fail-safe). - messenger — карточка. Дано: событие опубликовано. Когда
messengerобрабатывает. Тогда в чате сделкиDealIdпоявляется карточка «бронирование создано (Резерв доReserveExpiresAt)». - Несовместимый payload. Дано: сообщение без обязательного поля. Когда консюмер повторно не смог его обработать. Тогда сообщение уходит в DLQ, остальные брони обрабатываются.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: CalendarEventEntity, Entity / Aggregate: NotificationEntity
- API (8): GET /api/developer/crm/booking-programs — Программы бронирования, GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), POST /api/agent/crm/deals/{dealId}/bookings — Создать бронь (Резерв) от имени клиента, POST /api/developer/crm/booking-programs — Создать программу бронирования, POST /api/developer/crm/booking-programs/{programId}/archive — Архивировать программу бронирования, POST /api/pin-manager/crm/deals/{dealId}/timeline-note — Ручная запись в таймлайн, PUT /api/developer/crm/booking-programs/{programId} — Редактировать программу бронирования, PUT /api/investor/crm/bookings/{bookingId}/requisites — Выбрать реквизиты для брони
- Use Cases (3): UC-BKG-001. Инвестор инициирует бронирование с витрины, UC-BKG-002. Бронирование Резерв → Бронь, UC-CAL-007. Дедлайны брони CRM в календаре агента
- События (6): Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent), Event Contract: pin.crm.booking-cancelled (BookingCancelledEvent), Event Contract: pin.crm.booking-confirmed (BookingConfirmedEvent), Event Contract: pin.crm.booking-expired (BookingExpiredEvent), Event Contract: pin.crm.booking-paid (BookingPaidEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
- Консюмеры (2): Consumer: Calendar CRM Booking Sync (calendar-crm-booking-events), Consumer: DevStatsFunnelBookingCreatedConsumer
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- adr (1): ADR-0064 — контекст calendar (календарь и запись на показ/консультацию)
- scenarios (1): UC-OVR-003. Бронирование объекта у агента (+3a)
- index.md (1): Calendar Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность/дом полей: booking.md; условия программы:
booking-program.md; родительский процесс: deal.md;
CurrencySnapshot— snapshots.md. - Источник (операция): create-booking (будущий путь по конвенции); соседние контракты семейства: booking-confirmed.md, booking-paid.md, booking-expired.md, booking-cancelled.md.
- Статусы объекта недвижимости —
docs/06-services/chessboard/(консюмер booking-событий, будущий путь по конвенции); чат сделки — ADR-0055. - ADR: 0052 tenancy, 0056 simplicity-first.
- Обзор сервиса: README.md.