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 type | pin.messenger.contact-request-created |
| Класс события | ContactRequestCreatedEvent |
| Topic | pin.messenger.contact-request-created.v1 |
| Partitions / key | 3 / key = ContactId (порядок событий per-связи) |
| ContractVersion | 1 |
| Producer | messenger: отправка заявки (send-contact-request, дом contact.md) |
| Статус | draft |
Событие публикуется атомарно (в одном согласованном изменении) с созданием связи Contact (Pending)
через транзакционный outbox. Партиционирование по ключу ContactId даёт per-связь порядок с
contact-request-accepted.
Payload ContactRequestCreatedEvent
Корреляционные поля; ФИО/телефон сторон не входят (PII — резолв у consumer-а из реплики identity).
Дом полей — contact.md.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
ContactId | Guid | Да | Contact.Id | Immutable | Связь-заявка (Kafka key). Бизнес-ключ идемпотентности. |
RequesterUserId | Guid | Да | Contact.RequesterUserId | Immutable | Инициатор заявки. ФИО не в payload — из реплики identity у consumer-а. |
AddresseeUserId | Guid | Да | Contact.AddresseeUserId | Immutable | Адресат заявки — получатель уведомления. |
RequestMessage | string? | Нет | Contact.RequestMessage | Additive only | Сопроводительный текст заявки (≤512, вводится инициатором); null — без сообщения. |
CreatedAt | DateTimeOffset | Да | Contact.CreatedAt | Immutable | Момент подачи заявки (тайминг уведомления). |
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. - Порядок событий одной связи (
created→accepted) гарантирован ключомContactId; consumer держит дедуп поMessageId.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
ContactId, RequesterUserId, AddresseeUserId, CreatedAt | Internal | include (корреляция сторон; не PII) |
RequestMessage | PII (текст пользователя) | include — bounded (≤512), вводится инициатором для адресата; не в логи |
| ФИО / телефон сторон | PII | exclude — резолв у consumer-а из реплики identity; маскирование до Accepted — на стороне messenger |
Событие несёт только id + опциональный текст заявки. НЕ содержит ФИО/телефонов, IP/UA, secrets, tokens, платёжных данных.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/messenger/contacts/requests — Отправить заявку в контакты
- События (2): События сервиса messenger: реестр топиков, Event Contract: pin.messenger.contact-request-accepted (ContactRequestAcceptedEvent)
- registries (1): Реестр событий PIN
Связанные документы
- Источник (операция):
send-contact-request(API-карта в README); сущность — contact.md (ContactRequestCreated,ContactStatus, INV-CT-1/2/4/6); профиль/маскирование — messenger-user.md, guard blacklist — blacklist.md. - Парный контракт: contact-request-accepted.md; реестр — события сервиса.
- ADR: 0055 мини-мессенджер, 0056 simplicity-first.