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

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 — по этому дому; проекция в PromotionNotificationPayloadnotification.md.

Назначение

Consumer group notifications-promotion-events уведомляет покупателя пакета продвижения (менеджер застройщика/агентства) о смене статуса покупки: активирована, продлена, истекает, завершена. persist-then-send; кросс-контекстной записи нет.

Owning-сервис и триггер

ПолеЗначение
Сервис/контекстnotifications
Consumer groupnotifications-promotion-events
Producerpromotion (forward, ADR-0060)
ContractVersionожидается 1; несовпадение — warn-and-proceed

Топик и маппинг события

ТопикСобытие → NotificationKindПолучательUrgency
pin.promotion.purchase-changed.v1PromotionPurchaseChanged (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ТипОбяз.Как используется
PurchaseIdGuidДаid для PromotionNotificationPayload + dedup
TenantIdGuid (= CompanyId)Даполучатель fan-out — менеджеры компании-покупателя (actor-id в payload не входит, PII exclude)
PackageKind/StatusenumДаPackageKind/Status
Targets[]{ TargetKind, TargetId }[]ДаTargets[] { Kind, Id } (ref-модели)
EndsAtDateTimeOffset?НетEndsAt (для ExpiringSoon/Expired)

Алгоритм

Общая логика постановки в очередь — crm-events.md:

  1. Восстановить контекст события и сверить ContractVersion (несовпадение — warn-and-proceed); проверить идемпотентность по идентификатору сообщения — дубль пропускается (ack, no-op). Для ExpiringSoon срочность уведомления повышается до Critical.
  2. Проверить payload; при нарушении контракта события — NTF_INVALID_PAYLOAD (в DLQ).
  3. Определить получателей — менеджеры компании-покупателя (по TenantId = CompanyId); применить дедуп; если получателей нет — no-recipients (ack).
  4. Для каждого получателя определить каналы по виду и срочности, резолвить шаблон и адрес (адрес — мягкая деградация при отсутствии); применить тихие часы (ExpiringSoon как Critical тихие часы игнорирует). Если шаблон не резолвится — NTF_TEMPLATE_NOT_RESOLVED (в DLQ).
  5. Создать уведомление в статусе Pending и отрендерить тело; повтор события не создаёт дубль (dedup по ключу fan-out, INV-NTF-1). Уведомление и отметка идемпотентности фиксируются в одном согласованном изменении (persist-then-send).
  6. Успех → ack; retriable-сбой → retry; non-retriable → DLQ.

Диаграмма

Идемпотентность и replay

  • Consumer-level по MessageId; business-level — dedup-unique (INV-NTF-1). Replay: да.

Retry и DLQ

СбойКодRetryDLQ
Инфраструктурныйbackoff + пауза партицииНет
Нарушение payloadNTF_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).
  • ExpiringSoonCritical: срочность повышается, тихие часы игнорируются (дедлайн-уведомление).
  • Неизвестное значение 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, Then ignored-event (ack).

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

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

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