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

Event Contract: pin.messenger.contact-request-accepted (ContactRequestAcceptedEvent)

Версионируемый контракт интеграционного события «заявка в контакты подтверждена». Consumer-документы (notifications) ссылаются сюда, не переописывая payload. Событие фиксирует факт подтверждения связи (ContactStatus: Pending → Accepted, INV-CT-3/5) — контакт стал двусторонним: обе стороны получают полный профиль и право звонка. notifications уведомляет инициатора о подтверждении без синхронного запроса в messenger. Дом полей связи — contact.md; право звонка — call-session.md.

Назначение

При Accept заявка Pending становится Accepted: обе стороны видят полный профиль друг друга (ФИО + телефон, messenger-user.md) и получают право звонка (call-session.md, INV-CALL-1). Событие даёт notifications триггер уведомить инициатора (RequesterUserId) о подтверждении (offline push/email); in-app push обеим сторонам (ContactRequestAccepted в hub) идёт отдельно (persist-then-push). Разблокировка звонков — внутридоменный эффект messenger (guard INV-CT-5 над CallSession), а не отдельный consumer. PII (ФИО/телефон) в payload не входит — резолв у consumer-а из реплики identity.

Metadata

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

Событие публикуется атомарно (в одном согласованном изменении) с переходом Pending → Accepted через транзакционный outbox. Партиционирование по ключу ContactId кладёт событие в один раздел со своим contact-request-created — порядок жизненного цикла связи.

Payload ContactRequestAcceptedEvent

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

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
ContactIdGuidДаContact.IdImmutableСвязь (Kafka key). Бизнес-ключ идемпотентности.
RequesterUserIdGuidДаContact.RequesterUserIdImmutableИнициатор заявки — получатель уведомления о подтверждении.
AddresseeUserIdGuidДаContact.AddresseeUserIdImmutableАдресат, подтвердивший связь.
RespondedByUserIdGuidДаContact.RespondedByUserIdImmutableКто подтвердил (= AddresseeUserId, INV-CT-3).
RespondedAtDateTimeOffsetДаContact.RespondedAtImmutableМомент подтверждения (тайминг уведомления, авторитет — момент в messenger).

Все поля иммутабельны: Accepted — терминальный факт подтверждения (последующий RemoveContact даёт Cancelled и события подтверждения не отменяет; push удаления идёт через hub, не Kafka).

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

УсловиеПереход состоянияСогласованность
Адресат подтвердил заявку AcceptContactRequest (актор = AddresseeUserId, INV-CT-3)Pending → Acceptedсвязь Contact и факт подтверждения — в одном согласованном изменении

Decline (Pending → Declined) и RemoveContact (Accepted → Cancelled) Kafka-события не публикуют — только push в SignalR-hub. Создание заявки — контракт contact-request-created.md.

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

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

Разблокировка звонков (INV-CT-5 над CallSession) и in-app push обеим сторонам — внутридоменные эффекты messenger (не через это событие).

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

  • Порядок per-ContactId. Ключ ContactId кладёт created и accepted одной связи в один раздел — notifications видит их в порядке жизненного цикла (заявка → подтверждение). Между связями порядка нет.
  • At-least-once + идемпотентность. Доставка хотя бы раз; notifications дедуплицирует по идентификатору сообщения (MessageId, или ContactId как бизнес-ключ) — повторная доставка не шлёт второе уведомление инициатору.
  • Раскрытие профиля — не в событии. Полный профиль обеим сторонам открывает read-модель messenger по факту Accepted (INV-CT-5), а не consumer по этому событию; событие несёт только id + момент, ФИО/телефон consumer резолвит из своей реплики identity.
  • Retry/DLQ. Транзиентный сбой consumer-а — ретрай по offset; poison после исчерпания попыток → DLQ + алерт, offset продвигается (уведомление может опоздать, факт подтверждения в messenger уже необратим).
  • Терминальность. Accepted необратим: последующий RemoveContact даёт Cancelled и события подтверждения не отменяет (push удаления — через hub, не Kafka), поэтому compensating-события у контракта нет.

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

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

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

ПолеКласс данныхПравило
ContactId, RequesterUserId, AddresseeUserId, RespondedByUserId, RespondedAtInternalinclude (корреляция сторон; не PII)
ФИО / телефон сторонPIIexclude — резолв у consumer-а из реплики identity (профиль раскрывается обеим сторонам read-моделью messenger по факту Accepted)

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

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

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

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