Event Contract: pin.calendar.event-rescheduled (CalendarEventRescheduledEvent)
Версионируемый контракт интеграционного события «Календарное событие перенесено». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей агрегата —
entity: CalendarEventEntity.
Назначение
Фиксирует перенос события: у события в Planned/InProgress изменены StartAt/EndAt с сохранением
длительности (перенос — не переход статуса). Публичное: notifications уведомляет владельца и клиента о новом
времени (NotificationKind = CalendarEventRescheduled) без синхронного запроса в calendar. Напоминания при
переносе пересоздаются внутри calendar (event-reminder.md, правило 2) — это
не забота консюмеров.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.calendar.event-rescheduled |
| Topic | pin.calendar.event-rescheduled.v1 |
| Partitions / key | 3 (ADR-0056) / key = EventId (порядок событий одного календарного события) |
| ContractVersion | 1 |
| Producer | calendar: операция переноса владельцем (../api/agent/reschedule-event.md, только Origin = Manual); consumer booking-событий CRM (../consumers/crm-booking-events.md — перенос дедлайна при продлении брони, Origin = CrmBooking) |
| Статус | draft |
Событие публикуется атомарно с изменением агрегата (и пересозданием Pending-напоминаний в том же
согласованном изменении): пока изменение не зафиксировано, событие не отправляется; после фиксации —
доставляется гарантированно (at-least-once, transactional outbox), ключ = EventId.
События Origin = Appointment не переносятся: перенос записи инвестором — это атомарная пара «отмена
старой записи + новая запись» (appointment.md, INV-APT-5), которая порождает
appointment-cancelled + appointment-created и новое
календарное событие, а не reschedule существующего.
Payload CalendarEventRescheduledEvent
Дом полей — entity: CalendarEventEntity; здесь перечислено только то, что
реально в событии. StartAt/EndAt — новые значения после переноса.
| Поле | Тип | Обязательность | Источник (поле агрегата) | 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 (новые значения) | Вид события. |
Origin | string (enum EventOrigin) | Да | CalendarEventEntity.Origin | Additive only (новые значения) | Manual (перенос владельцем) / CrmBooking (продление дедлайна брони); Appointment не встречается (см. выше). |
ClientUserId | Guid? | Нет | CalendarEventEntity.ClientUserId | Additive only | Клиент-инвестор — получатель уведомления о новом времени; display-данные — из реплики identity. |
StartAt | DateTimeOffset | Да | CalendarEventEntity.StartAt | Immutable (значение на момент публикации) | Новое начало, UTC. |
EndAt | DateTimeOffset? | Нет | CalendarEventEntity.EndAt | Immutable (значение на момент публикации) | Новый конец, UTC; null — точечное событие. |
TimeZone | string | Да | CalendarEventEntity.TimeZone | Additive only | IANA-таймзона события — для отображения локального времени в уведомлении. |
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Владелец перенёс событие Origin = Manual (Planned/InProgress; длительность сохраняется) | статус не меняется; StartAt/EndAt изменены, Pending-напоминания пересозданы | атомарно с изменением агрегата |
Consumer booking-событий перенёс дедлайн Origin = CrmBooking (бронь продлена — новый BookingExpiresAt) | статус не меняется; StartAt = новый дедлайн | атомарно с изменением агрегата |
Каждый фактический перенос публикуется отдельным сообщением; при нескольких переносах подряд консюмер
применяет последнее по порядку в партиции (ключ = EventId — порядок гарантирован).
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
notifications | Уведомление о новом времени (NotificationKind = CalendarEventRescheduled): владельцу — при переносе consumer-ом (Origin = CrmBooking); клиенту ClientUserId (если задан) — при переносе владельцем. Консюмер — ../../notifications/consumers/calendar-events.md | MessageId | < 5 мин |
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом.v1.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking —
ContractVersion = 2+ новый топикpin.calendar.event-rescheduled.v2+ dual-publish + deadline миграции consumers + sunset.v1. - Unknown enum (
Kind/Origin) — fallback: сохранить строку как есть, warning, уведомление с нейтральным текстом (fail safe).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
EventId, TenantId, OwnerUserId, Kind, Origin, StartAt, EndAt, TimeZone | Internal | include |
ClientUserId | Internal | include (только идентификатор) |
- PII (ФИО/телефоны) в событии отсутствует по дизайну — display-данные консюмеры берут из своей реплики
pin.identity.users.v1;Title/Description/Locationв payload не входят. - Доступ к топику — m2m ACL Kafka (
notifications).
Доставка, retry/DLQ и восстановление
- Гарантия. At-least-once после атомарной публикации из outbox; ключ =
EventId— несколько переносов одного события приходят по порядку. Дубликаты возможны — консюмер идемпотентен поMessageId. - Retry. Транзиентный сбой консюмера → повтор с backoff; порядок по ключу сохраняется.
- DLQ. Неразбираемое/несовместимое сообщение после конечного числа попыток — в DLQ с алертом; остальные события не блокируются.
- Кросс-топик порядок. С event-status-changed порядок между топиками не гарантирован: если событие уже отменено/завершено на момент обработки переноса, уведомление о новом времени не отправляется (fail-safe по бизнес-состоянию, warning); финальную картину даёт уведомление об отмене/завершении.
- Промежуточное состояние. Новое время в календаре владельца видно сразу; уведомление клиенту — после обработки события (обычно < 5 мин).
Acceptance-критерии (контракт потребления)
- Перенос владельцем. Дано: событие
Consultation(Origin = Manual) сClientUserIdперенесено на день позже. Когдаnotificationsобрабатывает событие. Тогда клиенту уходит уведомлениеCalendarEventRescheduledс новым временем в таймзонеTimeZone. - Продление дедлайна. Дано: бронь продлена, consumer перенёс
BookingDeadline. Когдаnotificationsобрабатывает событие. Тогда владелец получает уведомление о новом дедлайне. - Двойной перенос. Дано: событие перенесено дважды подряд. Когда консюмер обрабатывает оба сообщения по порядку. Тогда итоговое уведомление отражает последнее время (каждое сообщение самодостаточно).
- Идемпотентность. Дано: сообщение доставлено дважды. Тогда дубль уведомления не создаётся.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: NotificationEntity
- API (1): POST /api/v1/agent/calendar/events/{id}/reschedule — Перенос события календаря
- Use Cases (1): US-реестр контекста calendar (US-CAL)
- События (4): Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent), Event Contract: pin.calendar.event-owner-changed (CalendarEventOwnerChangedEvent), 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; напоминания (пересоздание при переносе): event-reminder.md.
- Перенос записи (НЕ этот контракт): appointment.md INV-APT-5, appointment-cancelled.md + appointment-created.md.
- Соседние контракты семейства: event-created.md, event-status-changed.md, reminder-due.md.
- Producer-операции:
../api/agent/reschedule-event.md,../consumers/crm-booking-events.md; consumer:../../notifications/consumers/calendar-events.md. - ADR: 0064 calendar-контекст, 0052 tenancy.