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

Event Contract: pin.calendar.reminder-due (ReminderDueEvent)

Версионируемый контракт интеграционного события «Наступило напоминание» — материализованная строка напоминания достигла DueAt и обработана диспетчером. Consumer-документы ссылаются сюда, не переописывая payload. Дом полей — entity: EventReminderEntity; родительское событие — calendar-event.md.

Назначение

Фиксирует момент «пора напомнить»: фоновый диспетчер (../background/reminder-dispatch.md, цикл 60 с) перевёл строку напоминания Pending → Sent и опубликовал событие. Публичное: доставку по каналам (push, email и др.) выполняет notifications — сам calendar каналов доставки не знает и получателей не рассылает. Разделение ответственности: calendar отвечает за «когда и о чём напомнить», notifications — за «как и кому доставить».

Metadata

ПолеЗначение
Event typepin.calendar.reminder-due
Topicpin.calendar.reminder-due.v1
Partitions / key3 (ADR-0056) / key = EventId (все напоминания одного календарного события — в одной партиции, по порядку)
ContractVersion1
Producercalendar: диспетчер напоминаний ../background/reminder-dispatch.md (переход Pending → Sent, построчная блокировка)
Статусdraft

Событие публикуется атомарно с переходом строки Pending → Sent (transactional outbox): пока переход не зафиксирован, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = EventId. Ровно одно событие на строку напоминания (INV-REM-2).

Payload ReminderDueEvent

Дом полей — event-reminder.md и calendar-event.md; здесь перечислено только то, что реально в событии. Контракт самодостаточен: несёт всё нужное для текста напоминания (вид, время, таймзона, офсет) — notifications не запрашивает calendar синхронно. Title/Description/Location события в payload не входят (см. «Безопасность»).

ПолеТипОбязательностьИсточник (поле сущности)CompatibilityОписание
ReminderIdGuidДаEventReminderEntity.IdImmutableИдентификатор строки напоминания — ключ дедупликации у notifications.
EventIdGuidДаEventReminderEntity.CalendarEventIdImmutableРодительское календарное событие (= Kafka key).
TenantIdGuidДаCalendarEventEntity.TenantIdImmutableКомпания владельца календаря (ADR-0052).
OwnerUserIdGuidДаCalendarEventEntity.OwnerUserIdImmutableВладелец календаря — обязательный получатель напоминания.
ClientUserIdGuid?НетCalendarEventEntity.ClientUserIdAdditive onlyКлиент-инвестор — второй получатель, если у события есть клиент; display-данные — из реплики identity.
Kindstring (enum EventKind)ДаCalendarEventEntity.KindAdditive only (новые значения)Вид события: Showing / Consultation / Meeting / Task / BookingDeadline — определяет текст напоминания.
StartAtDateTimeOffsetДаCalendarEventEntity.StartAtImmutable (значение на момент публикации; при переносе строки пересоздаются)Начало события, UTC.
TimeZonestringДаCalendarEventEntity.TimeZoneAdditive onlyIANA-таймзона события — для локального времени в тексте напоминания.
OffsetMinutesintДаEventReminderEntity.OffsetMinutesImmutableОфсет «за N минут до начала» — для текста («через сутки», «через час»).
AppointmentIdGuid?НетCalendarEventEntity.AppointmentIdImmutableПорождающая запись — заполнен для событий Origin = Appointment; по нему выбирается NotificationKind.

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

УсловиеПереход состоянияПубликация
Диспетчер выбрал строку Status = Pending с DueAt < now (построчная блокировка)Pending → Sent (SentAt = now)атомарно с переходом строки

Не публикуется: для строк, переведённых в Skipped (событие отменено/завершено до DueAt; офсет в прошлом на момент материализации), и для строк, удалённых пересозданием при переносе события (event-reminder.md, правила материализации 1–3).

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

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
notificationsДоставка напоминания по каналам получателя: NotificationKind = AppointmentReminder (если AppointmentId задан — напоминание о встрече по записи, получатели — владелец и клиент) / CalendarReminder (остальные события, получатель — владелец; клиент ClientUserId добавляется, если задан). Текст — вид события + локальное время (StartAt + TimeZone) + горизонт (OffsetMinutes). Консюмер — ../../notifications/consumers/calendar-events.mdReminderId (бизнес-ключ; дубликаты доставки не порождают второго напоминания)< 1 мин (напоминание time-critical: суммарная точность — ~1 цикл диспетчера + доставка)

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

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

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

ПолеКласс данныхПравило
ReminderId, EventId, TenantId, OwnerUserId, ClientUserId, Kind, StartAt, TimeZone, OffsetMinutes, AppointmentIdInternalinclude (только идентификаторы, время и enum)
Title, Description, Location событияInternal / потенциально PIIexclude — в payload не входят по дизайну; текст напоминания строит notifications из Kind/времени
  • PII (ФИО/телефоны) в событии отсутствует по дизайну: display-данные получателей notifications берёт из своей реплики pin.identity.users.v1.
  • Доступ к топику — m2m ACL Kafka (notifications).

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

  • Гарантия. At-least-once после атомарной публикации из outbox; ключ = EventId — напоминания одного события приходят по порядку. Ровно одно событие на строку напоминания (INV-REM-2); дубликаты возможны только на уровне доставки — консюмер дедуплицирует по ReminderId.
  • Точность. Штатное опоздание — до ~1 цикла диспетчера (60 с) + окно доставки; SLA консюмера < 1 мин. Просроченные при простое диспетчера строки публикуются при восстановлении (напоминание лучше поздно, чем никогда — в пределах разумного: строки, чей StartAt уже прошёл, notifications вправе не доставлять, fail safe).
  • Гонка с отменой события. Если отмена события успела раньше диспетчера — строки Skipped, событие не публикуется; если диспетчер успел первым — напоминание уходит, а следом придёт уведомление об отмене (event-status-changed / appointment-cancelled) — допустимое окно (event-reminder.md).
  • Retry. Транзиентный сбой консюмера → повтор с backoff; партиция не продвигается дальше застрявшего оффсета, порядок по ключу сохраняется.
  • DLQ. Неразбираемое/несовместимое сообщение после конечного числа попыток — в DLQ с алертом; напоминания других событий не блокируются.

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

  • Напоминание о встрече. Дано: событие Origin = Appointment (AppointmentId задан), строка с OffsetMinutes = 60 сработала. Когда notifications обрабатывает событие. Тогда владельцу и клиенту уходит AppointmentReminder с локальным временем встречи.
  • Напоминание владельцу. Дано: Task без клиента, строка сработала. Когда notifications обрабатывает. Тогда только владельцу уходит CalendarReminder.
  • Дедупликация. Дано: сообщение с одним ReminderId доставлено дважды. Тогда напоминание доставлено получателям ровно один раз.
  • Ровно одно на строку. Дано: событие с офсетами {1440, 60}. Когда обе строки сработали. Тогда опубликовано ровно два reminder-due (по одному на строку), получатели получили два напоминания с разным горизонтом.
  • Несовместимый payload. Дано: сообщение без обязательного поля. Когда консюмер повторно не смог его обработать. Тогда сообщение уходит в DLQ, остальные напоминания обрабатываются.

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

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

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