Consumer: Promotion Events → Notifications (notifications-promotion-events)
Маппит событие продвижения (буст) сервиса promotion в NotificationKind и ставит уведомление в очередь
доставки. Целевой агрегат — notification.md; общий алгоритм постановки в
очередь — crm-events.md.
Контракт. Топик
pin.promotion.purchase-changed.v1— дом promotion/events/promotion-purchase-changed.md (контекстpromotion, ADR-0060; переоформлен изpin.catalog.promotions.v1). Payload — по этому дому; проекция вPromotionNotificationPayload— notification.md.
Назначение
Consumer group notifications-promotion-events уведомляет покупателя пакета продвижения (менеджер
застройщика/агентства) о смене статуса покупки: активирована, продлена, истекает, завершена.
persist-then-send; кросс-контекстной записи нет.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | notifications |
| Consumer group | notifications-promotion-events |
| Producer | promotion (forward, ADR-0060) |
| ContractVersion | ожидается 1; несовпадение — warn-and-proceed |
Топик и маппинг события
| Топик | Событие → NotificationKind | Получатель | Urgency |
|---|---|---|---|
pin.promotion.purchase-changed.v1 | PromotionPurchaseChanged (Activated/Extended/ExpiringSoon/Expired) | покупатель пакета (менеджер застройщика/агентства) | Normal (ExpiringSoon → Critical) |
Поля уведомления — PromotionNotificationPayload (notification.md):
Purchase { Id }, PackageKind, Targets[] { Kind, Id }, Status, EndsAt?.
Контекст события
- Actor = система; tenant — по получателю (компания покупателя). Метаданные события (идентификатор сообщения, агрегат-источник, время) доступны из конверта события.
- Идемпотентность: consumer-level по ключу
notifications-promotion:{EventType}:{MessageId}; business-level — dedup-unique (INV-NTF-1).
Входные данные / payload события
| Поле payload | Тип | Обяз. | Как используется |
|---|---|---|---|
PurchaseId | Guid | Да | id для PromotionNotificationPayload + dedup |
TenantId | Guid (= CompanyId) | Да | получатель fan-out — менеджеры компании-покупателя (actor-id в payload не входит, PII exclude) |
PackageKind/Status | enum | Да | → PackageKind/Status |
Targets[] | { TargetKind, TargetId }[] | Да | → Targets[] { Kind, Id } (ref-модели) |
EndsAt | DateTimeOffset? | Нет | → EndsAt (для ExpiringSoon/Expired) |
Алгоритм
Общая логика постановки в очередь — crm-events.md:
- Восстановить контекст события и сверить
ContractVersion(несовпадение — warn-and-proceed); проверить идемпотентность по идентификатору сообщения — дубль пропускается (ack, no-op). ДляExpiringSoonсрочность уведомления повышается доCritical. - Проверить payload; при нарушении контракта события —
NTF_INVALID_PAYLOAD(в DLQ). - Определить получателей — менеджеры компании-покупателя (по
TenantId = CompanyId); применить дедуп; если получателей нет —no-recipients(ack). - Для каждого получателя определить каналы по виду и срочности, резолвить шаблон и адрес (адрес — мягкая
деградация при отсутствии); применить тихие часы (
ExpiringSoonкакCriticalтихие часы игнорирует). Если шаблон не резолвится —NTF_TEMPLATE_NOT_RESOLVED(в DLQ). - Создать уведомление в статусе
Pendingи отрендерить тело; повтор события не создаёт дубль (dedup по ключу fan-out, INV-NTF-1). Уведомление и отметка идемпотентности фиксируются в одном согласованном изменении (persist-then-send). - Успех → ack; retriable-сбой → retry; non-retriable → DLQ.
Диаграмма
Идемпотентность и replay
- Consumer-level по
MessageId; business-level — dedup-unique (INV-NTF-1). Replay: да.
Retry и DLQ
| Сбой | Код | Retry | DLQ |
|---|---|---|---|
| Инфраструктурный | — | backoff + пауза партиции | Нет |
| Нарушение payload | NTF_INVALID_PAYLOAD | Нет | Да + алерт |
| Шаблон не резолвится | NTF_TEMPLATE_NOT_RESOLVED | Нет | Да + алерт |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/no-recipients/enqueued/failed. - Metrics:
notifications_enqueued_total{kind,channel},notifications_consumer_lag_seconds{source=promotion}. - Безопасность: system-actor; уведомление несёт
TenantIdпокупателя; m2m ACL Kafka.
Кросс-сервисные контракты
Ожидает от promotion: at-least-once, устойчивый MessageId, партиционирование по AggregateId покупки
(порядок статусов одной покупки сохраняется в партиции), ContractVersion; непустые PurchaseId/TenantId,
PackageKind/Status, Targets[]; EndsAt для ExpiringSoon/Expired. Персональные id менеджеров в
payload не приходят (PII exclude).
Ожидает от identity: членство компании-покупателя — для резолва получателей (менеджеров) по
TenantId = CompanyId; недоступность резолва аудитории — retriable (backoff), как инфраструктурный сбой.
Предоставляет: NotificationEntity(Pending) менеджерам компании-покупателя; обратной записи в promotion
нет; исходящее событие — только dispatched (dispatcher).
Порядок, версии и неизвестные события
- Порядок статусов. Consumer оповещает по-событийно и не сверяет событие с текущим состоянием покупки
(не source of truth). При переупорядочивании (напр. запоздалый
ExpiringSoonпослеExpired) возможен «поздний» сигнал — вред низкий; кросс-событийного подавления в первой версии нет. Повтор безопасен (dedup INV-NTF-1). ExpiringSoon→Critical: срочность повышается, тихие часы игнорируются (дедлайн-уведомление).- Неизвестное значение
Status(новый под-статус) → не маппится →ignored-event(ack), лог; версия ниже/выше1— warn-and-proceed, отсутствие обязательного известного поля →NTF_INVALID_PAYLOAD(DLQ).
Постусловие
- Каждому менеджеру компании-покупателя по каждому разрешённому каналу создан ровно один
NotificationEntity(Pending); отметка идемпотентности и уведомления — в одном согласованном изменении; offset подтверждён после фиксации. ExpiringSoonдоставлено немедленно даже в тихие часы (Critical); прочие статусы уважают тихие часы на interruptive-каналах.- Обратной записи в
promotionнет.
Acceptance criteria
- Given
PromotionPurchaseChanged(Activated), When consumer резолвит менеджеров компании и enqueue, Then каждому менеджеру создаётсяNotificationEntity(Pending, Kind=PromotionPurchaseChanged). - Given
Status = ExpiringSoonв тихие часы, When материализация, Then уведомление создаётся безScheduledAtи доставляется сразу (Criticalигнорирует тихие часы). - Given резолв менеджеров недоступен, When определение получателей, Then обработка ретраится (backoff); после исчерпания — DLQ.
- Given повторная доставка того же события, When обработка, Then
skipped-duplicate(ack), дублей нет. - Given неизвестное значение
Status, When маппинг вKind, Thenignored-event(ack).
Обратные ссылки
Автоссылки: где используется этот документ.
- index.md (1): Notifications Service
Связанные документы
- Целевой агрегат: notification.md; общий алгоритм — crm-events.md; forward-контекст — ADR-0060 promotion; карта — README контекста.
- ADR: 0052, 0056.