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

Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent)

Версионируемый контракт интеграционного события «Календарное событие создано». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей агрегата — entity: CalendarEventEntity.

Назначение

Фиксирует появление нового элемента в рабочем календаре владельца (агента, консультанта ПИН) — независимо от пути появления (Origin): ручное создание, каскад бронирования слота, автособытие «дедлайн брони» из booking-событий CRM. Публичное: даёт notifications факт появления события без синхронного запроса в calendar — прежде всего для уведомления владельца об автособытиях (дедлайн брони, порождённый consumer-ом booking-событий).

Metadata

ПолеЗначение
Event typepin.calendar.event-created
Topicpin.calendar.event-created.v1
Partitions / key3 (ADR-0056) / key = EventId (порядок событий одного календарного события во всех топиках pin.calendar.event-*)
ContractVersion1
Producercalendar: ручное создание (../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Описание
EventIdGuidДаCalendarEventEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаCalendarEventEntity.TenantIdImmutableКомпания владельца календаря (агентство / ПИН, ADR-0052).
OwnerUserIdGuidДаCalendarEventEntity.OwnerUserIdImmutableВладелец календаря — агент или консультант ПИН.
Kindstring (enum EventKind)ДаCalendarEventEntity.KindAdditive only (новые значения)Вид события: Showing / Consultation / Meeting / Task / BookingDeadline.
Originstring (enum EventOrigin)ДаCalendarEventEntity.OriginAdditive only (новые значения)Происхождение: Manual / Appointment / CrmBooking — по нему консюмер выбирает реакцию.
Prioritystring (enum EventPriority)ДаCalendarEventEntity.PriorityAdditive only (новые значения)Приоритет: Low / Medium / High / Critical.
StartAtDateTimeOffsetДаCalendarEventEntity.StartAtImmutable (актуализируется событием event-rescheduled)Начало, UTC.
EndAtDateTimeOffset?НетCalendarEventEntity.EndAtImmutable (актуализируется событием event-rescheduled)Конец, UTC; null — точечное событие (Task/BookingDeadline).
TimeZonestringДаCalendarEventEntity.TimeZoneAdditive onlyIANA-таймзона, в которой событие имеет смысл (например Europe/Moscow).
ClientUserIdGuid?НетCalendarEventEntity.ClientUserIdAdditive onlyКлиент-инвестор (identity); display-данные консюмер берёт из своей реплики pin.identity.users.v1.
AppointmentIdGuid?НетCalendarEventEntity.AppointmentIdImmutableПорождающая запись (appointment.md); заполнен только при Origin = Appointment.
DealIdGuid?НетCalendarEventEntity.DealIdImmutableСделка CRM (логическая ссылка); обязателен при Kind = BookingDeadline.
BookingIdGuid?НетCalendarEventEntity.BookingIdImmutableБронь 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.mdMessageId< 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, TimeZoneInternalinclude
ClientUserId, AppointmentId, DealId, BookingIdInternalinclude (только идентификаторы)
Title, Description, Location, MeetingLinkInternal / потенциально PIIexclude — в 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, остальные события обрабатываются.

Обратные ссылки

Автоссылки: где используется этот документ.

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