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

Event Contract: pin.crm.lead-participants-changed (LeadParticipantsChangedEvent)

Версионируемый контракт интеграционного события «участники лида изменены». Consumer-документы (messenger, notifications) ссылаются сюда, не переописывая payload. Фиксирует любое изменение активного состава участников лида (LeadParticipantEntity): добавление участника, снятие участия, смену роли, передачу ведения (смену исполнителя/ответственного). Дом полей агрегата — entity: LeadEntity (раздел LeadParticipantEntity — участники лида).

Назначение

Событие даёт messenger актуальный состав участников чата лида (ChatKind.Lead, ADR-0055, ADR-0071 п.7) без синхронного запроса в crm, а notifications — сигнал уведомить добавленных/удалённых участников. Чат лида создаётся по lead-created; при последующих изменениях состава (добавление участника, снятие участия, смена роли, передача ведения — см. lead.md, инварианты состава ADR-0071) messenger синхронизирует участников комнаты. Полный активный состав (ActiveParticipants) позволяет применять идемпотентную set-реконсиляцию (пригласить недостающих, убрать лишних), а дельта (Participants с ChangeType) — точечно обновлять роли и слать уведомления.

Каскад увольнения агента

Событие также штатно триггерится каскадом увольнения/исключения агента (ADR-0072): массовый soft-remove участий уволенного агента в процессах агентства публикует lead-participants-changed, по которому messenger снимает доступ бывшего участника (история команде сохраняется), а notifications уведомляет о смене состава.

Metadata

ПолеЗначение
Event typepin.crm.lead-participants-changed
Topicpin.crm.lead-participants-changed.v1
Partitions / key3 (crm — низкообъёмный, 1–10k активных лидов, ADR-0056) / key = AggregateId = LeadId (per-lead порядок изменений на одной партиции)
ContractVersion1
Producercrm: добавление/снятие участника, смена роли, передача ведения (Executor/Responsible), каскад увольнения агента (lead.md)
Статусdraft

Событие публикуется атомарно с мутацией агрегата: пока изменение состава не зафиксировано, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = LeadId.

Payload LeadParticipantsChangedEvent

Дом полей — entity: LeadEntity / LeadParticipantEntity (раздел «Участники лида» + «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии. Событие несёт и полный активный состав (для идемпотентного синка), и дельту изменения (для точечных действий и уведомлений). PII в payload отсутствует — только UserId.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
LeadIdGuidДаLeadEntity.IdImmutableКлюч агрегата (= Kafka key). Чат-комната лида в messenger (ChatKind.Lead).
TenantIdGuidДаLeadEntity.TenantIdImmutableAgencyCompanyId агентства-владельца лида (ADR-0052, ADR-0071).
ParticipantsList<LeadParticipantChange>Да (≥ 1)дельта операцииAdditive onlyЧто изменилось в этом событии (модель ниже).
ActiveParticipantsList<LeadParticipantSnapshot>Даактивные LeadParticipantEntity (RemovedAt is null)Additive onlyПолный активный состав после изменения — для set-реконсиляции чата лида.
ChangedByUserIdGuid?Нетактор операции (AddedByUserId / RemovedByUserId участия)Additive onlyКто изменил состав; null — система (авто-состав при создании лида / каскад увольнения / backfill).
ChangedAtDateTimeOffsetДамомент мутации (AddedAt/RemovedAt/апдейт роли)Additive onlyВремя изменения состава.
VersionlongДаLeadEntity.VersionImmutable semanticsДоменная версия лида; монотонный guard порядка у потребителя.

Вложенная модель LeadParticipantChange

ПолеТипОбязательностьОграниченияОписание
UserIdGuidДалогическая ссылка на identity.users(id)Пользователь-участник (LeadParticipantEntity.UserId).
Rolestring (enum ParticipantRole)Дазначения — lead.md / ADR-0071Роль после изменения; для Removed — роль на момент снятия.
ChangeTypestring (enum LeadParticipantChangeType)ДаAdded / Removed / RoleChanged / ExecutorChanged / ResponsibleChangedХарактер изменения участия (модель ниже).

Вложенная модель LeadParticipantSnapshot

ПолеТипОбязательностьОграниченияОписание
UserIdGuidДассылка на identity.users(id)Активный участник (RemovedAt is null).
Rolestring (enum ParticipantRole)Дазначения — lead.mdТекущая роль участника.

Enum LeadParticipantChangeType

Enum контракта события; передаётся строкой.

ЗначениеУсловиеИсточник в агрегате
AddedДобавлен новый участникдобавление участника (AddedAt проставлен)
RemovedУчастие снятоснятие участия (RemovedAt проставлен; в т.ч. каскад увольнения ADR-0072)
RoleChangedСменилась роль существующего участникасмена роли участника
ExecutorChangedПередача ведения: назначен новый активный Executorатомарная передача ведения (ExecutorAgentUserId изменён)
ResponsibleChangedСменился активный Responsible (ответственный/куратор)назначение/снятие Responsible (ResponsibleAgentUserId изменён)

Role — enum ParticipantRole (единый для лида и сделки; на лиде — Executor / Responsible / CoAgent / Assistant / Lawyer / InvestorPrimary / InvestorSecondary; SellManager / PinManager только на сделке); дом значений и инварианты состава (ровно один активный Executor, ровно один активный InvestorPrimary, InvestorSecondary — сколько угодно, уникальность UserId среди активных, процесс нельзя оставить без Executor) — lead.md / ADR-0071.

Без PII

Событие не содержит ФИО/телефонов: messenger резолвит отображаемые имена из собственной реплики pin.identity.users.v1 по UserId.

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

УсловиеПереход состоянияПубликация
Добавлен новый активный участникбез смены LeadStatus; Version++атомарно со сменой состояния агрегата
Снято участие (RemovedAt проставлен; в т.ч. каскад увольнения)без смены LeadStatus; Version++та же
Смена роли участникабез смены LeadStatus; Version++та же
Передача ведения (новый Executor) / смена Responsibleбез смены LeadStatus; Version++та же (дельта c ExecutorChanged/ResponsibleChanged)

Изменение состава не меняет LeadStatus и не дублирует lead-created (начальный состав при создании лида уходит в контекст создания; отдельного lead-participants-changed на создание не публикуется). Одно изменение состава → одно событие с дельтой в Participants и полным ActiveParticipants. Передача ведения — атомарная пара Removed (прежний Executor) + ExecutorChanged (новый Executor) в одном событии: процесс никогда не остаётся без Executor (ADR-0071).

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

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
messengerСинхронизация участников чата лида (ChatKind.Lead): set-реконсиляция комнаты по ActiveParticipants (пригласить недостающих, убрать отсутствующих), обновление ролей по Participants; системное сообщение о смене состава; при Removed доступ и realtime-подписка сняты, история сохранена команде (ADR-0072)MessageId + монотонный Version-guard на LeadId< 1 мин
notificationsУведомление добавленным (Added — «вас добавили в лид») и удалённым (Removed — «вас исключили из лида») участникам; уведомление о передаче ведения (ExecutorChanged) новому исполнителюMessageId (дедуп)< 1 мин

Порядок событий одного лида гарантирован ключом LeadId (одна партиция). Потребитель держит Version-guard: событие с Version не больше уже применённого — пропускается (устаревшая доставка). ActiveParticipants делает обработку самодостаточной — при пропуске отдельной дельты состав всё равно сходится.

Доставка, порядок и обработка сбоев

  • Гарантия публикации. Событие уходит только после фиксации изменения состава (Version++); при откате не публикуется. Доставка — at-least-once, ключ LeadId.
  • Порядок и version-guard. События одного лида упорядочены ключом LeadId (одна партиция). Потребитель держит монотонный Version-guard: событие с Version не больше уже применённого — пропускается (устаревшая доставка). При гонке двух изменений состава побеждает большее Version; более раннее, доставленное позже, отбрасывается guard-ом.
  • Самодостаточность (set-реконсиляция). Даже при пропуске отдельной дельты ActiveParticipants даёт полный актуальный состав — messenger сходится к нему реконсиляцией (пригласить недостающих, убрать лишних).
  • Идемпотентность потребителя. Дедуп по MessageId + Version-guard; повторная доставка не меняет уже сошедшийся состав.
  • Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки.
  • DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно (реконсиляция по ActiveParticipants идемпотентна).

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

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом .v1.
  • Additive-only для minor: новые optional-поля с безопасным default, новые значения ParticipantRole / LeadParticipantChangeType — без bump.
  • Breaking (удаление/переименование/смена типа или смысла) — новый ContractVersion = 2 + новый топик pin.crm.lead-participants-changed.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
Fail-safe для неизвестных значений

Unknown enum (Role / ChangeType) — fallback: сохранить строку как есть, warning; неизвестный ChangeType трактовать как no-op для дельты и опираться на ActiveParticipants (fail safe — состав всё равно синхронен). Неизвестное значение ParticipantRole — участник сохраняется в составе строкой + warning (additive-роль не ломает реконсиляцию).

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

ПолеКласс данныхПравило
LeadId, TenantId, VersionInternalinclude
Participants[].UserId, ActiveParticipants[].UserId, ChangedByUserIdInternal (идентификатор, не PII)include
Participants[].Role, Participants[].ChangeType, ActiveParticipants[].RoleInternalinclude
ChangedAtInternalinclude
  • Событие не переносит raw PII: ФИО и телефоны участников в payload отсутствуют — только UserId. Отображаемые имена/контакты messenger берёт из своей реплики pin.identity.users.v1; при логировании producer/consumer телефоны/ФИО не фигурируют.
  • Доступ к топику ограничен m2m ACL Kafka (producer crm, consumers messenger, notifications).
  • Событие НЕ содержит secrets, tokens, credentials, эскроу/платёжных реквизитов, документов, вложений.

Наблюдаемость

  • Логи (без PII): факт публикации lead-participants-changed с LeadId/Version, ChangeType и Role дельты (UserId — идентификатор, не PII); ФИО/телефоны участников в логах не фигурируют.
  • Метрики: число изменений состава по топику; сквозная задержка синхронизации чата лида у messenger (< 1 мин); доля событий передачи ведения (ExecutorChanged); число срабатываний Version-guard (индикатор переупорядочения/гонок); глубина DLQ (порог алерта > 0).
  • Доступ: топик — m2m ACL Kafka (producer crm, consumers messenger, notifications); PII в событие не входит.

Acceptance Criteria

  • Given лид с активным составом, When добавлен новый участник, Then Version++ и ровно одно lead-participants-changed публикуется с дельтой (ChangeType = Added) в Participants и полным ActiveParticipants.
  • Given снятие участия / смена роли, When изменение фиксируется, Then событие с ChangeType = Removed/RoleChanged; LeadStatus не меняется.
  • Given передача ведения, When назначен новый Executor, Then событие несёт атомарную пару Removed (прежний) + ExecutorChanged (новый) в Participants, в ActiveParticipants ровно один активный Executor (процесс не остаётся без исполнителя).
  • Given каскад увольнения агента (ADR-0072), When участие агента снято, Then публикуется lead-participants-changed с ChangeType = Removed; messenger снимает доступ, история сохраняется.
  • Given событие с Version не больше уже применённого, When потребитель обрабатывает, Then оно пропускается (устаревшая доставка).
  • Given пропущена отдельная дельта, When приходит следующее событие, Then состав чата лида всё равно сходится к ActiveParticipants (set-реконсиляция).
  • Given создание лида, When формируется начальный состав, Then lead-participants-changed не публикуется (начальный состав уходит в lead-created).
  • Given неизвестный ChangeType / Role, When потребитель обрабатывает, Then дельта/роль трактуется fail-safe (no-op для дельты, роль строкой), а состав берётся из ActiveParticipants + warning.
  • Given breaking-изменение payload, When новый контракт, Then pin.crm.lead-participants-changed.v2 + dual-publish + sunset .v1.

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

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

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

  • Сущность (дом полей LeadEntity / LeadParticipantEntity + ParticipantRole): lead.md.
  • Соседние события лида: lead-created.md, lead-expired.md.
  • Аналог для сделки (единая модель участников): deal-members-changed.md.
  • Контекст чата лида — ../../messenger/ (ChatKind.Lead), ADR-0055.
  • Реплика имён участников у messenger: user-upserted.md.
  • ADR: ADR-0071 (единая модель участников, ParticipantRole, Executor/Responsible, видимость по составу), ADR-0072 (каскад увольнения), ADR-0052 (TenantId = AgencyCompanyId).