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 type | pin.crm.lead-participants-changed |
| Topic | pin.crm.lead-participants-changed.v1 |
| Partitions / key | 3 (crm — низкообъёмный, 1–10k активных лидов, ADR-0056) / key = AggregateId = LeadId (per-lead порядок изменений на одной партиции) |
| ContractVersion | 1 |
| Producer | crm: добавление/снятие участника, смена роли, передача ведения (Executor/Responsible), каскад увольнения агента (lead.md) |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата: пока изменение состава не зафиксировано, событие не
отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = LeadId.
Payload LeadParticipantsChangedEvent
Дом полей — entity: LeadEntity / LeadParticipantEntity (раздел «Участники лида» +
«Доменные и интеграционные события»); здесь перечислено только то, что реально в событии. Событие несёт
и полный активный состав (для идемпотентного синка), и дельту изменения (для точечных действий и
уведомлений). PII в payload отсутствует — только UserId.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
LeadId | Guid | Да | LeadEntity.Id | Immutable | Ключ агрегата (= Kafka key). Чат-комната лида в messenger (ChatKind.Lead). |
TenantId | Guid | Да | LeadEntity.TenantId | Immutable | AgencyCompanyId агентства-владельца лида (ADR-0052, ADR-0071). |
Participants | List<LeadParticipantChange> | Да (≥ 1) | дельта операции | Additive only | Что изменилось в этом событии (модель ниже). |
ActiveParticipants | List<LeadParticipantSnapshot> | Да | активные LeadParticipantEntity (RemovedAt is null) | Additive only | Полный активный состав после изменения — для set-реконсиляции чата лида. |
ChangedByUserId | Guid? | Нет | актор операции (AddedByUserId / RemovedByUserId участия) | Additive only | Кто изменил состав; null — система (авто-состав при создании лида / каскад увольнения / backfill). |
ChangedAt | DateTimeOffset | Да | момент мутации (AddedAt/RemovedAt/апдейт роли) | Additive only | Время изменения состава. |
Version | long | Да | LeadEntity.Version | Immutable semantics | Доменная версия лида; монотонный guard порядка у потребителя. |
Вложенная модель LeadParticipantChange
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
UserId | Guid | Да | логическая ссылка на identity.users(id) | Пользователь-участник (LeadParticipantEntity.UserId). |
Role | string (enum ParticipantRole) | Да | значения — lead.md / ADR-0071 | Роль после изменения; для Removed — роль на момент снятия. |
ChangeType | string (enum LeadParticipantChangeType) | Да | Added / Removed / RoleChanged / ExecutorChanged / ResponsibleChanged | Характер изменения участия (модель ниже). |
Вложенная модель LeadParticipantSnapshot
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
UserId | Guid | Да | ссылка на identity.users(id) | Активный участник (RemovedAt is null). |
Role | string (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.
Событие не содержит ФИО/телефонов: 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.
Unknown enum (Role / ChangeType) — fallback: сохранить строку как есть, warning; неизвестный
ChangeType трактовать как no-op для дельты и опираться на ActiveParticipants (fail safe — состав всё
равно синхронен). Неизвестное значение ParticipantRole — участник сохраняется в составе строкой + warning
(additive-роль не ломает реконсиляцию).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
LeadId, TenantId, Version | Internal | include |
Participants[].UserId, ActiveParticipants[].UserId, ChangedByUserId | Internal (идентификатор, не PII) | include |
Participants[].Role, Participants[].ChangeType, ActiveParticipants[].Role | Internal | include |
ChangedAt | Internal | include |
- Событие не переносит raw PII: ФИО и телефоны участников в payload отсутствуют — только
UserId. Отображаемые имена/контактыmessengerберёт из своей репликиpin.identity.users.v1; при логировании producer/consumer телефоны/ФИО не фигурируют. - Доступ к топику ограничен m2m ACL Kafka (producer
crm, consumersmessenger,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, consumersmessenger,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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: ChatEntity (+ParticipantEntity как owned-child), Entity / Aggregate: NotificationEntity, Entity: ParticipantEntity (owned-child ChatEntity)
- API (5): DELETE /api/agent/crm/leads/{leadId}/participants/{userId} — Снять участника лида, POST /api/agent/crm/leads/{leadId}/participants — Добавить участника лида, POST /api/agent/crm/leads/{leadId}/primary-investor — Назначить основного инвестора лида, PUT /api/agent/crm/{deals|leads}/{id}/executor — Передать ведение (сменить исполнителя), PUT /api/agent/crm/{deals|leads}/{id}/responsible — Назначить/снять ответственного (куратора)
- Use Cases (6): UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель, UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора, UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора), UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-MSG-008. Добавленный участник появляется в чате сделки/лида, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (1): Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent)
- Консюмеры (2): Consumer: CRM Events → Notifications (notifications-crm-events), Consumer: Lead Chat Participants Sync (MessengerLeadParticipantsChangedConsumer)
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом полей
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).