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 type | pin.calendar.event-status-changed |
| Topic | pin.calendar.event-status-changed.v1 |
| Partitions / key | 3 (ADR-0056) / key = EventId (порядок переходов одного календарного события) |
| ContractVersion | 1 |
| Producer | calendar: операции владельца (../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 | Описание |
|---|---|---|---|---|---|
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 — по нему консюмер решает, строить ли уведомление (для Appointment — no-op, см. «Потребители»). |
Status | string (enum EventStatus) | Да | CalendarEventEntity.Status | Additive only (новые значения) | Новый статус: InProgress / Completed / Cancelled (Planned не встречается — см. выше). |
CancelReason | string? | Нет (обязателен при Status = Cancelled) | CalendarEventEntity.CancelReason | Additive only | Причина отмены (INV-CAL-7); свободный текст — класс Internal, маскирование в логах. |
ClientUserId | Guid? | Нет | CalendarEventEntity.ClientUserId | Additive only | Клиент-инвестор — получатель уведомления об отмене; display-данные — из реплики identity. |
StartAt | DateTimeOffset | Да | CalendarEventEntity.StartAt | Immutable (значение на момент публикации) | Начало события, UTC — для текста уведомления. |
TimeZone | string | Да | CalendarEventEntity.TimeZone | Additive only | IANA-таймзона события — для отображения локального времени в уведомлении. |
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Владелец начал событие (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/Expired | Planned → 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 уведомление владельцу «дедлайн брони снят»; Appointment — no-op: уведомления участникам строят более специфичные appointment-cancelled / appointment-completed (событие фильтруется по Origin, дублей нет). Консюмер — ../../notifications/consumers/calendar-events.md | MessageId | < 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, TimeZone | Internal | include |
ClientUserId | Internal | include (только идентификатор) |
CancelReason | Internal (свободный текст) | include; маскирование в логах producer/consumer |
Title, Description, Location, MeetingLink | Internal / потенциально PII | exclude — в 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, остальные события обрабатываются.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: NotificationEntity
- Use Cases (1): US-реестр контекста calendar (US-CAL)
- События (6): Event Contract: pin.calendar.appointment-cancelled (AppointmentCancelledEvent), Event Contract: pin.calendar.appointment-completed (AppointmentCompletedEvent), Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent), Event Contract: pin.calendar.event-owner-changed (CalendarEventOwnerChangedEvent), Event Contract: pin.calendar.event-rescheduled (CalendarEventRescheduledEvent), Event Contract: pin.calendar.reminder-due (ReminderDueEvent)
- Консюмеры (2): Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: Calendar Events → Notifications (notifications-calendar-events)
- registries (1): Реестр событий PIN
Связанные документы
- Сущность/дом полей: calendar-event.md (матрица переходов INV-CAL-2, терминальность INV-CAL-7); напоминания (Skipped при отмене/завершении): event-reminder.md.
- Соседние контракты семейства: event-created.md, event-rescheduled.md, reminder-due.md; каскады записи: appointment-cancelled.md, appointment-completed.md.
- Producer-операции:
../api/agent/start-event.md,../api/agent/complete-event.md,../api/agent/cancel-event.md,../consumers/crm-booking-events.md; consumer:../../notifications/consumers/calendar-events.md. - ADR: 0064 calendar-контекст, 0052 tenancy, 0056 simplicity-first.