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

Consumer: Investor-Owner Events → Notifications (notifications-investor-owner-events)

Назначение

Группа notifications-investor-owner-events слушает события investor-owner (объекты инвестора-собственника) и уведомляет инвестора-собственника о появлении объекта в его портфеле, продаже объекта и привязке к нему как к собственнику (NotificationEntity). persist-then-send; кросс-контекстной записи нет. Общий алгоритм enqueue — как в crm-events.md.

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

ПолеЗначение
Сервис/контекстnotifications (Pin.Notifications.Background)
Consumer groupnotifications-investor-owner-events
Обрабатываемые событиятопики pin.investor-owner.*.v1 (таблица ниже)
Producerinvestor-owner
ContractVersionожидается 1; несовпадение — warn-and-proceed
Эффект (событие → что меняется)создаются NotificationEntity (Pending) для инвестора-собственника

Топики и маппинг

ТопикСобытие → NotificationKindПолучательUrgency
pin.investor-owner.property-added.v1PropertyAddedинвестор-собственникNormal
pin.investor-owner.property-sold.v1PropertySoldинвестор-собственникNormal
pin.investor-owner.owner-linked.v1OwnerLinkedпривязанный инвестор-собственникNormal

Дом полей payload — investor-owner/events/property-added.md, property-sold.md, owner-linked.md; поля уведомления — OwnershipNotificationPayload (notification.md).

Контекст события

  • Из метаданных события доступны MessageId, CreatedAt, Version, AggregateId; actor = system; получатель — инвестор, обычно TenantId = null (вне tenant, ADR-0052).
  • Идемпотентность: consumer-level ключ notifications-investor-owner:{EventType}:{MessageId}; business-level — unique (SourceMessageId, RecipientUserId, Channel, Kind) (INV-NTF-1).

Входные данные / payload события

Поле payloadТипОбяз.Как используется
PropertyObjectId/OwnerUserIdGuidДаполучатель + id для OwnershipNotificationPayload
PropertyObjectNameстрокаДаPropertyObject { Id, Name }
OwnerNamestring?НетOwner { Id, Name }

Алгоритм

Общая логика enqueue (резолв каналов/шаблона/адреса, материализация, согласованность) — как в crm-events.md.

  1. Контекст события. Восстановить метаданные и сверить ContractVersion (несовпадение — warn-and-proceed); consumer-идемпотентность по notifications-investor-owner:{EventType}:{MessageId} (дубль → skipped-duplicate, ack). Определить Kind по типу события.
  2. Проверка входа. Обязательные идентификаторы непусты; иначе → NTF_INVALID_PAYLOAD (DLQ).
  3. Резолв получателя. Инвестор-собственник; дедуп; пусто → no-recipients (ack).
  4. Резолв доставки и материализация. По получателю — каналы для Kind/Urgency; шаблон (Kind, Locale, Channel) (нет активного → NTF_TEMPLATE_NOT_RESOLVED, DLQ); адрес и локаль — снапшот из контактной реплики recipient-contact.md (INV-RC-4/5, graceful); тихие часы; создаётся Notification(Pending) с рендером (dedup INV-NTF-1). Отправку выполняет dispatcher; собственных интеграционных событий consumer не публикует.
  5. Согласованность и результат. Уведомления и отметка идемпотентности фиксируются в одном согласованном изменении, offset — после фиксации. Успех/no-recipients → 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=investor-owner}.
  • Безопасность: system-actor; получатель — инвестор (TenantId = null); ФИО собственника — PII (маска); m2m ACL Kafka.

Кросс-сервисные контракты

Ожидает от investor-owner (upstream source of truth): доставку at-least-once с устойчивым MessageId (Guid v7 — ключ дедупликации), партиционирование по AggregateId объекта (порядок событий одного объекта сохраняется в партиции), заголовок ContractVersion; непустые PropertyObjectId/OwnerUserId и display-поле PropertyObjectName. ФИО/контакты получателя в событии не приходят — адрес/локаль берутся снапшотом из контактной реплики recipient-contact.md на enqueue (не синхронный lookup).

Предоставляет: набор NotificationEntity(Pending) на пару (получатель × канал) (dedup INV-NTF-1); обратной записи в investor-owner нет. Единственное исходящее интеграционное событие — dispatched (публикует dispatcher при Sent/терминальном Failed, не этот consumer).

Порядок, версии и неизвестные события

  • Порядок/несогласованность. property-addedproperty-sold по одному объекту — независимые факты, каждый даёт своё уведомление; кросс-событийной сверки consumer не делает (не source of truth портфеля). Повтор и переупорядочивание топика безопасны: дубль отсекается по (SourceMessageId, RecipientUserId, Channel, Kind) (INV-NTF-1).
  • Неизвестный тип события на pin.investor-owner.*.v1 (появился новый под-тип) → не маппится в Kindignored-event (ack, лог; не DLQ) — forward-compatible.
  • Версия ниже/выше ожидаемой (1) — warn-and-proceed по известным (additive-only) полям; отсутствие обязательного известного поля → NTF_INVALID_PAYLOAD (DLQ).

Постусловие

  • Для каждого разрешённого настройками канала получателя создан ровно один NotificationEntity(Pending); отметка идемпотентности зафиксирована в том же согласованном изменении; offset подтверждён после фиксации.
  • In-app (если включён и разрешён) доставлен realtime в течение секунд; внешние каналы — заглушки: пока канал выключен, уведомление по нему не создаётся, и до включения провайдера получатель видит только in-app.
  • Обратная запись в investor-owner не выполнялась; исходных фактов consumer не менял.

Acceptance criteria

  • Given событие PropertyAdded по объекту инвестора-собственника при включённом in-app, When consumer его обрабатывает, Then создаётся NotificationEntity(Pending, Kind=PropertyAdded) для собственника и доставляется realtime.
  • Given то же событие доставлено повторно (replay), When consumer его обрабатывает, Then новых уведомлений не создаётся (skipped-duplicate, ack).
  • Given событие с пустым OwnerUserId/PropertyObjectId, When проверка входа, Then NTF_INVALID_PAYLOAD, событие в DLQ + алерт.
  • Given нет Active-шаблона (Kind, Locale) ни под один канал получателя, When резолв шаблона, Then NTF_TEMPLATE_NOT_RESOLVED, DLQ + алерт.
  • Given событие неизвестного нового под-типа на топике, When маппинг в Kind, Then ignored-event (ack), уведомления не создаются.

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

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

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