Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent)
Версионируемый контракт интеграционного события «Календарное событие создано». Consumer-документы ссылаются
сюда, не переописывая payload. Дом полей агрегата —
entity: CalendarEventEntity.
Назначение
Фиксирует появление нового элемента в рабочем календаре владельца (агента, консультанта ПИН) — независимо от
пути появления (Origin): ручное создание, каскад бронирования слота, автособытие «дедлайн брони» из
booking-событий CRM. Публичное: даёт notifications факт появления события без синхронного запроса в
calendar — прежде всего для уведомления владельца об автособытиях (дедлайн брони, порождённый
consumer-ом booking-событий).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.calendar.event-created |
| Topic | pin.calendar.event-created.v1 |
| Partitions / key | 3 (ADR-0056) / key = EventId (порядок событий одного календарного события во всех топиках pin.calendar.event-*) |
| ContractVersion | 1 |
| Producer | calendar: ручное создание (../api/agent/create-event.md), каскад бронирования слота (../api/public/book-slot.md), consumer booking-событий CRM (../consumers/crm-booking-events.md) |
| Статус | draft |
Событие публикуется атомарно с созданием агрегата (для Origin = Appointment — атомарно со всей связкой
«слот Booked + запись + событие»): пока изменение не зафиксировано, событие не отправляется; после фиксации
— доставляется гарантированно (at-least-once, transactional outbox), ключ = EventId.
Payload CalendarEventCreatedEvent
Дом полей — entity: CalendarEventEntity (раздел «Доменные и интеграционные
события»); здесь перечислено только то, что реально в событии. Title/Description/Location в payload
не входят — могут содержать персональные данные клиента (см. «Безопасность»).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
EventId | Guid | Да | CalendarEventEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | CalendarEventEntity.TenantId | Immutable | Компания владельца календаря (агентство / ПИН, ADR-0052). |
OwnerUserId | Guid | Да | CalendarEventEntity.OwnerUserId | Immutable | Владелец календаря — агент или консультант ПИН. |
Kind | string (enum EventKind) | Да | CalendarEventEntity.Kind | Additive only (новые значения) | Вид события: Showing / Consultation / Meeting / Task / BookingDeadline. |
Origin | string (enum EventOrigin) | Да | CalendarEventEntity.Origin | Additive only (новые значения) | Происхождение: Manual / Appointment / CrmBooking — по нему консюмер выбирает реакцию. |
Priority | string (enum EventPriority) | Да | CalendarEventEntity.Priority | Additive only (новые значения) | Приоритет: Low / Medium / High / Critical. |
StartAt | DateTimeOffset | Да | CalendarEventEntity.StartAt | Immutable (актуализируется событием event-rescheduled) | Начало, UTC. |
EndAt | DateTimeOffset? | Нет | CalendarEventEntity.EndAt | Immutable (актуализируется событием event-rescheduled) | Конец, UTC; null — точечное событие (Task/BookingDeadline). |
TimeZone | string | Да | CalendarEventEntity.TimeZone | Additive only | IANA-таймзона, в которой событие имеет смысл (например Europe/Moscow). |
ClientUserId | Guid? | Нет | CalendarEventEntity.ClientUserId | Additive only | Клиент-инвестор (identity); display-данные консюмер берёт из своей реплики pin.identity.users.v1. |
AppointmentId | Guid? | Нет | CalendarEventEntity.AppointmentId | Immutable | Порождающая запись (appointment.md); заполнен только при Origin = Appointment. |
DealId | Guid? | Нет | CalendarEventEntity.DealId | Immutable | Сделка CRM (логическая ссылка); обязателен при Kind = BookingDeadline. |
BookingId | Guid? | Нет | CalendarEventEntity.BookingId | Immutable | Бронь CRM; обязателен при Kind = BookingDeadline. |
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Владелец создал событие вручную (Origin = Manual) | [*] → Planned | атомарно с созданием агрегата |
Инвестор забронировал слот — каскад создал событие (Origin = Appointment) | [*] → Planned (атомарно со слотом Available → Booked и записью) | атомарно со всей связкой; параллельно публикуется appointment-created |
Consumer booking-событий CRM породил автособытие «дедлайн брони» (Origin = CrmBooking, Kind = BookingDeadline) | [*] → Planned | атомарно с созданием агрегата |
Дальнейший жизненный цикл события публикуется отдельными контрактами: перенос — event-rescheduled.md, переходы статуса — event-status-changed.md, напоминания — reminder-due.md.
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
notifications | Уведомление владельцу календаря по правилам Origin/Kind: для Origin = CrmBooking — «дедлайн брони добавлен в календарь»; для Origin = Appointment уведомления участникам строит более специфичный appointment-created (это событие консюмер фильтрует по Origin, дублей нет); Origin = Manual — по правилам notifications (собственное действие владельца обычно не уведомляется). Консюмер — ../../notifications/consumers/calendar-events.md | MessageId | < 5 мин |
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом.v1.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.calendar.event-created.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum (
Kind/Origin/Priority) — fallback: сохранить строку как есть, warning, обработать как событие общего вида (уведомление с нейтральным текстом либо no-op — fail safe).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
EventId, TenantId, OwnerUserId, Kind, Origin, Priority, StartAt, EndAt, TimeZone | Internal | include |
ClientUserId, AppointmentId, DealId, BookingId | Internal | include (только идентификаторы) |
Title, Description, Location, MeetingLink | Internal / потенциально PII | exclude — в payload не входят по дизайну |
- PII (ФИО/телефоны) в событии отсутствует по дизайну: display-данные владельца и клиента консюмеры
берут из своей реплики
pin.identity.users.v1. Если персональные данные всё же попадут в логи producer/consumer при отладке — маскирование обязательно. - Доступ к топику — m2m ACL Kafka (
notifications). - Событие НЕ содержит secrets, tokens, raw credentials, вложений.
Доставка, retry/DLQ и восстановление
- Гарантия. At-least-once после атомарной публикации из outbox; ключ =
EventId— порядок событий одного календарного события в топике сохранён. Дубликаты возможны (переотправка) — консюмер обязан быть идемпотентен. - Идемпотентность консюмера.
notifications— поMessageId: повторная доставка не создаёт дубль уведомления. - Retry. Транзиентный сбой консюмера (недоступность своей БД/зависимости) → повтор с backoff; партиция не продвигается дальше застрявшего оффсета, порядок по ключу сохраняется.
- DLQ. Неразбираемое/несовместимое сообщение (ошибка десериализации, отсутствуют обязательные поля) после конечного числа попыток уходит в DLQ с алертом; сообщения других событий не блокируются.
- Кросс-топик порядок. Между
event-createdи более позднимиevent-rescheduled/event-status-changedодного события межтопиковый порядок не гарантирован; консюмер применяет реакцию по бизнес-состоянию (устаревшее «создано» после уже обработанной отмены — no-op, warning). - Промежуточное состояние. Событие в календаре владельца видно сразу (данные
calendar); уведомление приходит после обработки (обычно < 5 мин) — штатное окно eventual-consistency.
Acceptance-критерии (контракт потребления)
- Автособытие дедлайна. Дано: consumer booking-событий создал
BookingDeadline(Origin = CrmBooking). Когдаnotificationsобрабатываетevent-created. Тогда владельцу уходит уведомление о новом дедлайне брони (BookingId/DealIdв payload заполнены). - Событие записи не дублирует уведомления. Дано: бронирование слота породило событие
(
Origin = Appointment) и запись. Когдаnotificationsобрабатываетevent-created. Тогда уведомления участникам не создаются по этому событию — их создаёт appointment-created. - Идемпотентность. Дано: событие доставлено дважды. Когда обрабатывается повторно. Тогда второе уведомление не создаётся (no-op).
- Несовместимый payload. Дано: сообщение без обязательного поля. Когда консюмер повторно не смог его обработать. Тогда сообщение уходит в DLQ, остальные события обрабатываются.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: NotificationEntity
- API (2): POST /api/public/calendar/appointments — Самозапись на слот (бронирование), POST /api/v1/agent/calendar/events — Создание события календаря
- Use Cases (2): UC-CAL-003. Инвестор бронирует слот с витрины, US-реестр контекста calendar (US-CAL)
- События (5): Event Contract: pin.calendar.appointment-created (AppointmentCreatedEvent), Event Contract: pin.calendar.event-owner-changed (CalendarEventOwnerChangedEvent), Event Contract: pin.calendar.event-rescheduled (CalendarEventRescheduledEvent), Event Contract: pin.calendar.event-status-changed (CalendarEventStatusChangedEvent), Event Contract: pin.calendar.reminder-due (ReminderDueEvent)
- Консюмеры (1): Consumer: Calendar Events → Notifications (notifications-calendar-events)
- registries (1): Реестр событий PIN
Связанные документы
- Сущность/дом полей: calendar-event.md; порождающая запись: appointment.md; источник дедлайнов: booking-created.md.
- Соседние контракты семейства: event-rescheduled.md, event-status-changed.md, reminder-due.md.
- Consumer:
../../notifications/consumers/calendar-events.md; producer-операции:../api/agent/create-event.md,../api/public/book-slot.md,../consumers/crm-booking-events.md. - ADR: 0064 calendar-контекст, 0052 tenancy, 0056 simplicity-first.