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

Event Contract: pin.messenger.contact-request-created (ContactRequestCreatedEvent)

Версионируемый контракт интеграционного события «создана заявка в контакты». Consumer-документы (notifications) ссылаются сюда, не переописывая payload. Событие фиксирует факт создания заявки (ContactStatus: [*] → Pending, INV-CT-1/2/4/6) и несёт id сторон — notifications уведомляет адресата о входящей заявке без синхронного запроса в messenger и без раскрытия PII. Дом полей связи — contact.md; дом профиля сторон — messenger-user.md.

Назначение

Контакт-связь: инициатор (Requester) отправляет заявку адресату (Addressee); до подтверждения профиль маскирован (фамилия — первой буквой), звонок недоступен (contact.md, INV-CT-5). Событие даёт notifications триггер уведомить адресата о входящей заявке (offline push/email) — плюс in-app push ContactRequestReceived в SignalR-hub идёт отдельно (persist-then-push, не через Kafka). Событие сознательно не несёт ФИО/телефон: имя адресат резолвит из собственной реплики identity (маскирование до Accepted — на стороне read-модели messenger).

Metadata

ПолеЗначение
Event typepin.messenger.contact-request-created
Класс событияContactRequestCreatedEvent
Topicpin.messenger.contact-request-created.v1
Partitions / key3 / key = ContactId (порядок событий per-связи)
ContractVersion1
Producermessenger: отправка заявки (send-contact-request, дом contact.md)
Статусdraft

Событие публикуется атомарно (в одном согласованном изменении) с созданием связи Contact (Pending) через транзакционный outbox. Партиционирование по ключу ContactId даёт per-связь порядок с contact-request-accepted.

Payload ContactRequestCreatedEvent

Корреляционные поля; ФИО/телефон сторон не входят (PII — резолв у consumer-а из реплики identity). Дом полей — contact.md.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
ContactIdGuidДаContact.IdImmutableСвязь-заявка (Kafka key). Бизнес-ключ идемпотентности.
RequesterUserIdGuidДаContact.RequesterUserIdImmutableИнициатор заявки. ФИО не в payload — из реплики identity у consumer-а.
AddresseeUserIdGuidДаContact.AddresseeUserIdImmutableАдресат заявки — получатель уведомления.
RequestMessagestring?НетContact.RequestMessageAdditive onlyСопроводительный текст заявки (≤512, вводится инициатором); null — без сообщения.
CreatedAtDateTimeOffsetДаContact.CreatedAtImmutableМомент подачи заявки (тайминг уведомления).

ContactId/RequesterUserId/AddresseeUserId иммутабельны (связь неизменна по сторонам); повторная заявка на активную пару отклоняется (INV-CT-2) и события не порождает.

Когда публикуется

УсловиеПереход состоянияСогласованность
Создана заявка SendContactRequest (INV-CT-1 не-self, INV-CT-2 нет активной связи, INV-CT-4 не в blacklist, INV-CT-6 обе стороны существуют)[*] → Pendingсвязь Contact и факт создания — в одном согласованном изменении

Decline/Cancel заявки Kafka-события не публикуют — только push в SignalR-hub (ContactRequestDeclined/ContactRemoved, дом contact.md). Подтверждение — отдельный контракт contact-request-accepted.md.

Потребители (consumers)

Consumer-контекстЭффект (событие → что меняется)ИдемпотентностьЗадержка (SLA)
notificationsУведомление адресату (AddresseeUserId) о входящей заявке (offline push/email; ФИО инициатора — из реплики identity consumer-а)first-seen по MessageId (или ContactId)< 1 мин

In-app realtime (ContactRequestReceived в hub) идёт из messenger напрямую (persist-then-push), не через notifications.

Гарантии доставки, порядок и обработка отказов

  • Порядок per-ContactId. Ключ ContactId кладёт created (и последующий accepted) одной связи в один раздел — consumer видит их в порядке жизненного цикла. Между связями порядка нет.
  • At-least-once + идемпотентность. Доставка хотя бы раз; notifications дедуплицирует по идентификатору сообщения (MessageId, или ContactId) — повтор не шлёт второе уведомление адресату.
  • Только создание порождает событие. Повторная заявка на активную пару отклоняется (INV-CT-2) и события не порождает; Decline/Cancel заявки — только in-app push через hub (внешнего потребителя нет). Единственная связь created для пары гарантируется доменом до публикации.
  • Retry/DLQ. Транзиентный сбой consumer-а — ретрай по offset; poison после исчерпания попыток → DLQ + алерт, offset продвигается (уведомление о заявке может опоздать; сама заявка Pending в messenger уже зафиксирована и видна адресату по REST/hub).

Версионирование и совместимость

  • ContractVersion = 1; при несовпадении версии входящего события с текущей consumer логирует warning и продолжает (апкаст схемы не выполняется). Топик кодирует версию суффиксом .v1 по конвенции.
  • Additive-only для minor: новые поля — optional с безопасным default, без bump.
  • Breakingновый ContractVersion = 2 + топик pin.messenger.contact-request-created.v2 + dual-publish + deadline миграции consumers + sunset .v1.
  • Порядок событий одной связи (createdaccepted) гарантирован ключом ContactId; consumer держит дедуп по MessageId.

Безопасность

ПолеКласс данныхПравило
ContactId, RequesterUserId, AddresseeUserId, CreatedAtInternalinclude (корреляция сторон; не PII)
RequestMessagePII (текст пользователя)include — bounded (≤512), вводится инициатором для адресата; не в логи
ФИО / телефон сторонPIIexclude — резолв у consumer-а из реплики identity; маскирование до Accepted — на стороне messenger

Событие несёт только id + опциональный текст заявки. НЕ содержит ФИО/телефонов, IP/UA, secrets, tokens, платёжных данных.

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

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

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