Event Contract: pin.notifications.dispatched
Версионируемый контракт интеграционного события «уведомление доставлено / окончательно не доставлено». Опциональное событие аналитики доставки — единственный дом полезной нагрузки перечислен здесь; consumer-документы (при появлении) ссылаются сюда. Событие несёт только идентификаторы/enum/метки — тела и PII не входят.
Назначение
Даёт потенциальным потребителям (будущий аналитический контекст / dev-stats) факт финального исхода доставки
NotificationEntity (../domain/notification.md) без чтения БД notifications:
переход в Sent (успех) или терминальный Failed (исчерпан retry). В первой версии прод-потребителей нет
(статус draft, ADR-0056: без событий
без потребителей) — контракт зафиксирован для будущей аналитики. Публикуется через транзакционный outbox
dispatcher-ом/retry-worker-ом.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.notifications.dispatched |
| Topic | pin.notifications.dispatched.v1 |
| Partitions / key | 3 партиции (низкий объём, ADR-0056) / key = NotificationId (= AggregateId) |
| ContractVersion | 1 |
| Producer | notifications: dispatcher (переход Sent) и retry-worker (терминальный Failed) |
| Статус | draft (нет прод-потребителей; переезд/эволюция без dual-publish) |
Событие публикуется через транзакционный outbox атомарно с переходом статуса. Ключ агрегата
AggregateId = NotificationId,AggregateType = "Notification";MessageId(Guid v7) — идемпотентность у потребителя;Version=ContractVersion.
Payload
Дом полей — entity: NotificationEntity («Доменные и интеграционные события»);
все поля — скаляры/enum-строки, вложенных моделей и PII нет.
| Поле | Тип | Обязательность | Источник (NotificationEntity) | Compatibility | Описание |
|---|---|---|---|---|---|
NotificationId | Guid | Да | Id | Immutable | Ключ агрегата (= AggregateId, Kafka key). |
RecipientUserId | Guid | Да | RecipientUserId | Immutable | Получатель (id, не PII). |
TenantId | Guid? | Нет | TenantId | Additive only | Компания получателя (null — инвестор). |
Kind | string (enum NotificationKind) | Да | Kind | Additive only | Вид уведомления. |
Channel | string (enum Channel) | Да | Channel | Additive only | Канал доставки. |
Status | string (enum NotificationStatus) | Да | Status | Additive only | Итог: Sent / Failed (терминальный). |
SourceService | string | Да | SourceService | Immutable | Контекст-источник исходного события. |
Attempts | int | Да | Attempts | Additive only | Число попыток отправки. |
LastErrorCode | string? | Нет | из LastError | Additive only | Безопасный код ошибки (для Failed); текст/PII не входят. |
DispatchedAt | DateTimeOffset | Да | SentAt/FailedAt | Immutable | Момент финального исхода. |
Тело/заголовок/адрес/Payload уведомления в событие не входят — только исход доставки.
Когда публикуется
| Условие | Переход состояния | Транзакция |
|---|---|---|
| Успешная отправка провайдером/in-app | Pending/Failed → Sent | статус + outbox в одном согласованном изменении |
Исчерпан retry (Attempts >= MaxAttempts) | Failed (терминально) | то же |
Промежуточные Failed (будет retry) событие не порождают — только финальные исходы (снижение шума).
Гарантии доставки, порядка и идемпотентности
- Доставка — at-least-once. Публикация через транзакционный outbox атомарно с переходом статуса: факт не
теряется при падении после коммита, но при повторной публикации outbox-а возможна повторная доставка →
потребитель обязан дедуплицировать по
MessageId(first-seen). - Порядок — per-
NotificationId. Ключ партиционирования =NotificationId, поэтому события одного уведомления упорядочены в партиции; глобального порядка между разными уведомлениями нет и не требуется (каждый исход — независимый факт аналитики). - Эмиссия — не более одного финала на канал-экземпляр. Одно
NotificationEntityпорождает максимум одноSentи максимум одно терминальноеFailed; переходFailed → Sent(успешный retry) даёт толькоSent; промежуточныеFailedподавлены. Дубль на стороне брокера/replay-а снимается дедупом поMessageId.
Потребители (consumers)
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
| (будущий аналитический / dev-stats) | — (нет прод-потребителей в v1) | агрегация доставляемости по Kind/Channel/SourceService | first-seen по MessageId | — |
Версионирование и совместимость (реальность SDK)
ContractVersion = 1; при несовпадении версии события потребитель логирует warning и продолжает. Топик кодирует версию суффиксом.v1.- Additive-only для minor: новые optional-поля/enum-значения — без bump.
- Breaking (удаление/переименование/смена типа) —
ContractVersion = 2+pin.notifications.dispatched.v2; т.к. статусdraftбез прод-потребителей — переезд без периода dual-publish.
Безопасность
Событие несёт только идентификаторы, enum и метку — прямого PII нет по построению (адрес/тело/Payload
исключены).
| Поле | Класс данных | Правило |
|---|---|---|
NotificationId, RecipientUserId, TenantId | Internal | include (id, не PII) |
Kind, Channel, Status, SourceService, LastErrorCode | Internal | include (enum/код) |
Attempts, DispatchedAt | Internal | include |
Событие НЕ содержит secrets, tokens, адрес доставки, тело/заголовок уведомления. Доступ к топику — m2m ACL Kafka.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: NotificationEntity
- Use Cases (2): UC-NTF-002. Событие сделки → многоканальное уведомление с учётом настроек, UC-NTF-003. Сбой провайдера → повтор доставки
- Консюмеры (2): Consumer: Calendar Events → Notifications (notifications-calendar-events), Consumer: CRM Events → Notifications (notifications-crm-events)
- Фоновые задачи (1): Background Job: NotificationRetryWorker
- index.md (1): Notifications Service
- registries (1): Реестр событий PIN
Связанные документы
- Источник (агрегат): ../domain/notification.md (
Status/Attempts, статусные события); dispatcher/retry — ../background/notification-retry.md. - README контекста.