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

Event Contract: pin.calendar.event-status-changed (CalendarEventStatusChangedEvent)

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

Назначение

Фиксирует каждый переход статуса календарного события: владелец начал событие (Planned → InProgress), завершил (→ Completed) или отменил (→ Cancelled); те же переходы происходят каскадами записи и consumer-ом booking-событий CRM. Публичное: notifications уведомляет владельца и клиента об отмене/завершении без синхронного запроса в calendar. Перевод Pending-напоминаний в Skipped при отмене/завершении выполняется внутри calendar (event-reminder.md, правило 3) — это не забота консюмеров.

Metadata

ПолеЗначение
Event typepin.calendar.event-status-changed
Topicpin.calendar.event-status-changed.v1
Partitions / key3 (ADR-0056) / key = EventId (порядок переходов одного календарного события)
ContractVersion1
Producercalendar: операции владельца (../api/agent/start-event.md, ../api/agent/complete-event.md, ../api/agent/cancel-event.md — только Origin = Manual); каскады переходов записи (appointment.md); consumers booking- и deal-событий CRM (../consumers/crm-booking-events.md, ../consumers/crm-deal-events.md)
Статусdraft

Событие публикуется атомарно с переходом статуса агрегата (и переводом Pending-напоминаний в Skipped для Completed/Cancelled в том же согласованном изменении): пока изменение не зафиксировано, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once, transactional outbox), ключ = EventId.

Начальный статус Planned этим контрактом не публикуется — создание события фиксирует event-created; перенос времени — event-rescheduled (не переход статуса).

Payload CalendarEventStatusChangedEvent

Дом полей — 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 — по нему консюмер решает, строить ли уведомление (для Appointment — no-op, см. «Потребители»).
Statusstring (enum EventStatus)ДаCalendarEventEntity.StatusAdditive only (новые значения)Новый статус: InProgress / Completed / Cancelled (Planned не встречается — см. выше).
CancelReasonstring?Нет (обязателен при Status = Cancelled)CalendarEventEntity.CancelReasonAdditive onlyПричина отмены (INV-CAL-7); свободный текст — класс Internal, маскирование в логах.
ClientUserIdGuid?НетCalendarEventEntity.ClientUserIdAdditive onlyКлиент-инвестор — получатель уведомления об отмене; display-данные — из реплики identity.
StartAtDateTimeOffsetДаCalendarEventEntity.StartAtImmutable (значение на момент публикации)Начало события, UTC — для текста уведомления.
TimeZonestringДаCalendarEventEntity.TimeZoneAdditive onlyIANA-таймзона события — для отображения локального времени в уведомлении.

Когда публикуется

УсловиеПереход состоянияПубликация
Владелец начал событие (Origin = Manual, start)Planned → InProgressатомарно с переходом агрегата
Владелец завершил событие (Origin = Manual, complete)Planned/InProgress → Completedатомарно с переходом агрегата
Каскад записи: исход Completed/NoShow (appointment.md, таблица каскадов)Planned → Completed (Origin = Appointment)атомарно с переходом записи и слота; параллельно публикуется appointment-completed
Владелец отменил событие (Origin = Manual, cancel; CancelReason обязателен)Planned/InProgress → Cancelledатомарно с переходом агрегата
Каскад записи: Declined/Cancelled/ExpiredPlanned → Cancelled (Origin = Appointment)атомарно с переходом записи и слота; параллельно публикуется appointment-cancelled
Consumer booking-событий: фаза брони завершилась штатно (резерв подтверждён booking-confirmed / оплачен booking-paid) — дедлайн исполненPlanned → Completed (Origin = CrmBooking)атомарно с изменением агрегата (../consumers/crm-booking-events.md, шаги 4–5)
Consumer booking-событий: бронь завершена/отменена/истекла — дедлайн снятPlanned → Cancelled (Origin = CrmBooking)атомарно с изменением агрегата; тот же переход выполняет consumer терминальных deal-событий (../consumers/crm-deal-events.md)

Потребители (consumers)

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
notificationsРеакция по Origin: Manual — при Cancelled уведомление клиенту ClientUserId (если задан) об отмене встречи; CrmBooking — при Cancelled уведомление владельцу «дедлайн брони снят»; Appointmentno-op: уведомления участникам строят более специфичные appointment-cancelled / appointment-completed (событие фильтруется по Origin, дублей нет). Консюмер — ../../notifications/consumers/calendar-events.mdMessageId< 5 мин

Версионирование и совместимость

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом .v1.
  • Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа или смысла) — ContractVersion = 2 + новый топик pin.calendar.event-status-changed.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum (Kind/Origin/Status) — fallback: сохранить строку как есть, warning; при неизвестном Origin уведомление не строится (fail safe — лучше пропустить, чем продублировать).

Безопасность

ПолеКласс данныхПравило
EventId, TenantId, OwnerUserId, Kind, Origin, Status, StartAt, TimeZoneInternalinclude
ClientUserIdInternalinclude (только идентификатор)
CancelReasonInternal (свободный текст)include; маскирование в логах producer/consumer
Title, Description, Location, MeetingLinkInternal / потенциально PIIexclude — в payload не входят по дизайну
  • PII (ФИО/телефоны) в событии отсутствует по дизайну: display-данные владельца и клиента консюмеры берут из своей реплики pin.identity.users.v1.
  • Доступ к топику — m2m ACL Kafka (notifications).
  • Событие НЕ содержит secrets, tokens, raw credentials, вложений.

Доставка, retry/DLQ и восстановление

  • Гарантия. At-least-once после атомарной публикации из outbox; ключ = EventId — переходы одного события приходят по порядку. Дубликаты возможны (переотправка) — консюмер идемпотентен по MessageId.
  • Retry. Транзиентный сбой консюмера (недоступность своей БД/зависимости) → повтор с backoff; партиция не продвигается дальше застрявшего оффсета, порядок по ключу сохраняется.
  • DLQ. Неразбираемое/несовместимое сообщение (ошибка десериализации, отсутствуют обязательные поля) после конечного числа попыток уходит в DLQ с алертом; сообщения других событий не блокируются.
  • Кросс-топик порядок. С event-rescheduled порядок между топиками не гарантирован: если на момент обработки перехода консюмер уже видел более позднее состояние, реакция применяется по бизнес-состоянию (устаревший переход — no-op, warning). Терминальные Completed/ Cancelled — финальная картина события (INV-CAL-7), после них новых переходов не будет.
  • Промежуточное состояние. Статус в календаре владельца виден сразу (данные calendar); уведомление клиенту — после обработки события (обычно < 5 мин) — штатное окно eventual-consistency.

Acceptance-критерии (контракт потребления)

  • Start. Дано: владелец начал Planned-событие. Когда переход зафиксирован. Тогда опубликовано ровно одно событие со Status = InProgress (не Cancelled — фикс бага черновика, ADR-0064 п.2).
  • Отмена с клиентом. Дано: Consultation (Origin = Manual) с ClientUserId отменена с причиной. Когда notifications обрабатывает событие. Тогда клиенту уходит уведомление об отмене со временем встречи в таймзоне TimeZone.
  • Каскад записи не дублирует уведомления. Дано: запись отклонена агентом — каскад отменил событие (Origin = Appointment). Когда notifications обрабатывает event-status-changed. Тогда уведомление по этому событию не создаётся — участников уведомляет appointment-cancelled.
  • Снятый дедлайн. Дано: бронь оплачена, consumer отменил BookingDeadline. Когда notifications обрабатывает событие. Тогда владелец получает уведомление «дедлайн брони снят».
  • Идемпотентность. Дано: сообщение доставлено дважды. Тогда дубль уведомления не создаётся (no-op).
  • Несовместимый payload. Дано: сообщение без обязательного поля. Когда консюмер повторно не смог его обработать. Тогда сообщение уходит в DLQ, остальные события обрабатываются.

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

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

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