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

UC-CAL-008. Напоминания о событиях календаря

Назначение

Описывает сквозной механизм напоминаний: из офсетов календарного события («за сутки», «за час») материализуются конкретные моменты срабатывания, фоновый диспетчер публикует наступившие напоминания, а notifications доставляет их по каналам владельцу события и клиенту (если он есть). Напоминания живут только жизненным циклом события: перенос пересоздаёт их, отмена/завершение — гасит. Связан с контекстом calendar.

Metadata

ПолеЗначение
IDUC-CAL-008
Контекстcalendar (см. docs/06-services/calendar/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (диспетчер напоминаний); получатели — владелец события (агент/консультант ПИН) и клиент-инвестор записи
Permissions / scopesystem-scope worker-а (ADR-0052); собственных API-операций у напоминаний нет
TenantЧерез родительское событие; диспетчер работает поверх всех tenant-ов
Record-levelОфсеты напоминаний управляются владельцем события (calendar.events.manage)

Предусловие

  • Существует событие с непустыми ReminderOffsetsMinutes (default {1440, 60}) — ручное (UC-CAL-001), от записи (UC-CAL-003) или дедлайн брони (UC-CAL-007).
  • У получателей настроены каналы доставки в notifications (иначе — деградация доставки на стороне notifications, календарь не затронут).

Триггер

ПолеЗначение
ИсточникSchedule
Триггерящий элементWorker ../background/reminder-dispatch.md — polling-цикл 60 с, построчная блокировка (ADR-0056)

Основной Поток

  1. При создании события для каждого офсета из ReminderOffsetsMinutes материализуется строка напоминания DueAt = StartAt − OffsetMinutes, статус Pending; офсет с уже прошедшим DueAt — сразу Skipped (задним числом напоминания не отправляются) — правила материализации в event-reminder.md.
  2. Диспетчер циклом (60 с) выбирает строки Pending с DueAt < now под построчной блокировкой (два экземпляра worker-а не обработают одну строку дважды).
  3. Каждую выбранную строку диспетчер переводит Pending → Sent (SentAt = now) и атомарно с переходом публикует pin.calendar.reminder-due.v1 (ReminderId, EventId, OwnerUserId, ClientUserId?, Kind, StartAt, TimeZone, OffsetMinutes, AppointmentId?; PII нет).
  4. notifications потребляет reminder-due (../../notifications/consumers/calendar-events.md) и доставляет по каналам: kind AppointmentReminder — по событиям записей (получатели — агент и инвестор), CalendarReminder — по остальным событиям (получатель — владелец). Дедупликация на стороне notifications — по ReminderId (доставка at-least-once).
  5. Владелец (и клиент) получают напоминание с локальным временем события (по TimeZone). Точность срабатывания — до ~1 цикла диспетчера (штатно ≤ 2 мин от DueAt).

Альтернативные Потоки

A1. Перенос события или смена офсетов

  1. При переносе события (UC-CAL-001, дедлайн — UC-CAL-007) или изменении ReminderOffsetsMinutes все Pending-строки события удаляются и набор материализуется заново от актуального StartAt (правило 1 применяется заново). Ранее отправленные Sent-строки не трогаются — исторический факт.

A2. Отмена или завершение события

  1. Переход события в Cancelled/Completed переводит все его Pending-строки в Skipped в том же согласованном изменении — напоминания об отменённой встрече не уходят.

A3. Гонка «диспетчер ↔ отмена события»

  1. Арбитраж — построчная блокировка: если отмена успела первой, диспетчер видит Skipped и пропускает; если первым успел диспетчер — напоминание уходит, следом приходит уведомление об отмене (допустимое окно, не ошибка).

A4. Падение и повтор диспетчера

  1. Переход Pending → Sent и публикация reminder-due неразделимы: «отправили, но не отметили» / «отметили, но не отправили» невозможно. Повторный проход видит Sent — no-op; ровно одно reminder-due на строку (INV-REM-2). Downstream-повторы доставки гасятся дедупликацией по ReminderId.

A5. Отставание диспетчера

  1. Диспетчер долго не работал (сбой) — накопившиеся Pending с прошедшим DueAt отправляются при восстановлении одним прогоном (напоминание всё ещё полезно, если событие впереди); мониторинг ловит отставание (alert: Pending с DueAt старше 5 мин). Строки событий, уже ставших Cancelled/Completed за время простоя, к этому моменту — Skipped и не отправляются.

A6. Событие без напоминаний

  1. Пустой ReminderOffsetsMinutes — строки не материализуются, напоминаний нет; событие живёт штатно.

Постусловие

  • Каждая наступившая Pending-строка ровно один раз переведена в Sent и породила ровно одно reminder-due; неактуальные строки — Skipped.
  • Для каждой Pending-строки DueAt согласован с актуальным StartAt события (INV-REM-1).
  • Получатели уведомлены notifications в пределах точности механизма.

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
ReminderDueEvent.v1pin.calendar.reminder-due.v1Переход строки Pending → Sent (шаг 3); key = EventIdnotifications (AppointmentReminder для событий записей, CalendarReminder для остальных)

Acceptance Criteria

#КритерийПроверка
AC1Событие за 3 дня до StartAt с офсетами {1440, 60} даёт две Pending-строки с DueAt = StartAt − 24 ч и StartAt − 1 чтест материализации
AC2Событие за 30 минут до StartAt с теми же офсетами даёт обе строки сразу Skipped, reminder-due не публикуетсятест прошедших офсетов
AC3Диспетчер переводит наступившую строку в Sent и публикует ровно одно reminder-due атомарно; повторный проход — no-opтест диспетчеризации и идемпотентности (A4)
AC4Перенос события удаляет Pending-строки и пересоздаёт от нового StartAt; Sent-строки неизменнытест переноса (A1)
AC5Отмена события переводит PendingSkipped, напоминания не уходяттест отмены (A2)
AC6Два параллельных экземпляра диспетчера не обрабатывают одну строку дважды (построчная блокировка)тест конкуренции
AC7По reminder-due события записи notifications доставляет AppointmentReminder агенту и инвестору; по ручному событию — CalendarReminder владельцуинтеграционный тест доставки
AC8Точность срабатывания ≤ 2 мин от DueAt при штатном цикле 60 снагрузочный/интеграционный тест

Наблюдаемость

  • Логи (без PII). reminder-sent (reminderId, eventId, tenantId, offsetMinutes, lagSeconds = now − DueAt); reminder-skipped с причиной.
  • Метрики. calendar_reminders_sent_total, calendar_reminders_skipped_total{reason}, calendar_reminder_dispatch_lag_seconds (p95; alert: Pending с DueAt старше 5 мин), длительность цикла диспетчера.

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

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

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

  • Domain: event-reminder.md (правила материализации, INV-REM-1/2, stateDiagram), calendar-event.md (ReminderOffsetsMinutes, ограничения офсетов).
  • Worker: ../background/reminder-dispatch.md; событие: ../events/reminder-due.md.
  • Доставка: ../../notifications/consumers/calendar-events.md, notification.md (kinds AppointmentReminder / CalendarReminder).
  • Паттерн polling + построчная блокировка: booking-expiration.md, ADR-0056.
  • Связанные UC: UC-CAL-001 (офсеты ручных событий), UC-CAL-003 (напоминания о встречах), UC-CAL-007 (напоминания о дедлайнах).