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 group | notifications-investor-owner-events |
| Обрабатываемые события | топики pin.investor-owner.*.v1 (таблица ниже) |
| Producer | investor-owner |
| ContractVersion | ожидается 1; несовпадение — warn-and-proceed |
| Эффект (событие → что меняется) | создаются NotificationEntity (Pending) для инвестора-собственника |
Топики и маппинг
| Топик | Событие → NotificationKind | Получатель | Urgency |
|---|---|---|---|
pin.investor-owner.property-added.v1 | PropertyAdded | инвестор-собственник | Normal |
pin.investor-owner.property-sold.v1 | PropertySold | инвестор-собственник | Normal |
pin.investor-owner.owner-linked.v1 | OwnerLinked | привязанный инвестор-собственник | 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/OwnerUserId | Guid | Да | получатель + id для OwnershipNotificationPayload |
PropertyObjectName | строка | Да | → PropertyObject { Id, Name } |
OwnerName | string? | Нет | → Owner { Id, Name } |
Алгоритм
Общая логика enqueue (резолв каналов/шаблона/адреса, материализация, согласованность) — как в crm-events.md.
- Контекст события. Восстановить метаданные и сверить
ContractVersion(несовпадение — warn-and-proceed); consumer-идемпотентность поnotifications-investor-owner:{EventType}:{MessageId}(дубль →skipped-duplicate, ack). ОпределитьKindпо типу события. - Проверка входа. Обязательные идентификаторы непусты; иначе →
NTF_INVALID_PAYLOAD(DLQ). - Резолв получателя. Инвестор-собственник; дедуп; пусто →
no-recipients(ack). - Резолв доставки и материализация. По получателю — каналы для
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 не публикует. - Согласованность и результат. Уведомления и отметка идемпотентности фиксируются в одном согласованном
изменении, offset — после фиксации. Успех/
no-recipients→ 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=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-added→property-soldпо одному объекту — независимые факты, каждый даёт своё уведомление; кросс-событийной сверки consumer не делает (не source of truth портфеля). Повтор и переупорядочивание топика безопасны: дубль отсекается по(SourceMessageId, RecipientUserId, Channel, Kind)(INV-NTF-1). - Неизвестный тип события на
pin.investor-owner.*.v1(появился новый под-тип) → не маппится вKind→ignored-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 проверка входа, ThenNTF_INVALID_PAYLOAD, событие в DLQ + алерт. - Given нет
Active-шаблона(Kind, Locale)ни под один канал получателя, When резолв шаблона, ThenNTF_TEMPLATE_NOT_RESOLVED, DLQ + алерт. - Given событие неизвестного нового под-типа на топике, When маппинг в
Kind, Thenignored-event(ack), уведомления не создаются.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (1): Event Contract: pin.investor-owner.property-added (InvestorPropertyAddedKafkaEvent)
- index.md (1): Notifications Service
Связанные документы
- Целевой агрегат: notification.md; общий алгоритм — crm-events.md; входные контракты — investor-owner/events/; карта — README контекста.
- ADR: 0052, 0056.