POST /api/agent/crm/leads/{leadId}/participants — Добавить участника лида
Добавление участника в активный состав лида (LeadParticipantEntity, единый ParticipantRole, ADR-0071):
со-агент, ассистент, юрист, куратор (Responsible), созаёмщик/соинвестор (InvestorSecondary). Видимость
лида и синк участников чата лида (ChatKind.Lead, messenger) строятся на этом составе, поэтому добавление
участника открывает ему доступ к лиду и его чату. Дом полей — lead.md (раздел
«Участники лида», LeadParticipantEntity).
Назначение
Дать агенту-исполнителю / куратору / руководителю агентства (или Менеджеру ПИН) расширить команду ведения
лида, добавив участника с одной из ролей состава. Responsible — сингулярная опциональная роль: назначается,
если куратора ещё нет. Добавление публикует pin.crm.lead-participants-changed.v1 с дельтой и полным
активным составом.
Операция не назначает исполнителя и не меняет основного инвестора: сингулярные роли Executor и
InvestorPrimary через это добавление не заводятся (передача ведения — атомарной сменой исполнителя;
основной инвестор — set-primary-investor.md).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent (JWT агента/куратора/руководителя агентства/Менеджера ПИН) |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/add-lead-participant.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (агент-исполнитель / куратор лида) / User (администратор-руководитель агентства) / User (Менеджер ПИН, platform-scope) |
| Auth policy / Permissions | crm.leads.manage |
| Scope (RBAC) | организация (агентство) — record-level по членству (ADR-0071); руководитель — весь TenantId в scope-иерархии (ADR-0025); Менеджер ПИН — платформа |
| Record-level | доступ = актор ∈ активные участники лида агентской роли (Executor/Responsible) или администратор/руководитель компании-владельца (AgencyCompanyId = TenantId) или Менеджер ПИН; иначе CRM_LEAD_FORBIDDEN |
| Tenant isolation | TenantId = AgencyCompanyId в каждом запросе; lead.TenantId обязан совпасть |
| Audit | добавление участия (AddedByUserId = actor, CreatedAt), Version лида |
Выполнить может только активный участник лида агентской роли (Executor/Responsible), администратор/
руководитель компании-владельца лида (AgencyCompanyId = TenantId) или Менеджер ПИН — иначе
CRM_LEAD_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/leads/{leadId}/participants |
| Success status | 201 (участник добавлен, возвращается актуальный состав) |
| Idempotency header | не применяется — бизнес-дедуп по паре (LeadId,UserId) среди активных (INV-LEAD-7) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = AgencyCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | Required | Record-level (членство) |
LeadId | route | Guid | Да | != Guid.Empty; существующий лид | — |
UserId | body | Guid | Да | != Guid.Empty; существующий (в т.ч. lazy) пользователь identity; не совпадает с активным участником лида | — |
Role | body | ParticipantRole (enum-строка) | Да | одно из значений, применимых к лиду: CoAgent / Assistant / Lawyer / Responsible / InvestorSecondary; Executor/InvestorPrimary/SellManager/PinManager — отклоняются (см. ошибки) | — |
Модель ответа LeadParticipantsModel
Read-проекция активного состава лида после изменения (дом полей — lead.md); здесь — только релевантная операции дельта. PII нет — только id и роли.
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
LeadId | Guid | Да | lead.Id | Идентификатор лида. |
Participants | List<LeadParticipantModel> | Да | активные LeadParticipantEntity (RemovedAt is null) | Полный активный состав после добавления. |
ExecutorAgentUserId | Guid | Да | lead.ExecutorAgentUserId | Текущий исполнитель (не меняется этой операцией). |
ResponsibleAgentUserId | Guid? | Нет | lead.ResponsibleAgentUserId | Куратор (может появиться, если добавлен Responsible). |
PrimaryInvestorUserId | Guid | Да | lead.PrimaryInvestorUserId | Основной инвестор (не меняется этой операцией). |
Version | long | Да | lead.Version | Для оптимистичной блокировки на клиенте. |
LeadParticipantModel
| Поле | Тип | Обязательность | Описание |
|---|---|---|---|
UserId | Guid | Да | Пользователь-участник. |
Role | ParticipantRole (enum-строка) | Да | Роль в составе. |
IsPrimary | bool | Да | Признак «основной» в своей роли (для InvestorPrimary). |
Алгоритм
- Контекст и доступ. Операцию выполняет участник лида агентской роли / руководитель агентства / Менеджер
ПИН (permission
crm.leads.manage) в рамкахTenantId. - Проверка входа и предусловий.
- формат:
LeadId != Guid.Empty,UserId != Guid.Empty;Role— известное значение, применимое к лиду; - лид существует в
TenantId— иначеCRM_LEAD_NOT_FOUND; - состояние:
lead.Status == Active(INV-LEAD-2) — иначеCRM_LEAD_NOT_ACTIVE; - record-level (членство, ADR-0071): актор ∈ активные участники агентской роли или руководитель
TenantIdили Менеджер ПИН — иначеCRM_LEAD_FORBIDDEN; - роль допустима к добавлению:
Executor/InvestorPrimaryне заводятся здесь (сингулярные ключевые роли),SellManager/PinManagerнеприменимы к лиду — иначеCRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED.
- формат:
- Проверка состава (INV-LEAD-7).
UserIdне должен уже быть активным участником лида — иначеCRM_LEAD_PARTICIPANT_DUPLICATE;- для сингулярной роли
Responsible: если активныйResponsibleуже есть —CRM_LEAD_PARTICIPANT_ROLE_CONFLICT(сменить куратора — снять текущего и добавить нового).
- Что читаем. Лид с активным составом участников (строка лида блокируется на время правки от гонки со
сменой состава/передачей ведения). Пользователь
identityне подгружается — проверяется по логической ссылке. - Бизнес-правила и переходы. Добавляется активное участие
LeadParticipantEntity { UserId, Role, IsPrimary = false, AddedByUserId = actor }. Производные лида не меняются, кромеResponsibleAgentUserId(приRole = Responsible— назначается добавленным). Статус лида:Active → Active. Заявки на фиксацию и сделки лида не затрагиваются. - Что меняется. Появляется строка
crm.lead_participants(активная), обновляютсяUpdatedAt/Versionлида (иResponsibleAgentUserIdпри добавлении куратора) — атомарно;RowVersionдаёт оптимистичную блокировку → при конфликтеCRM_LEAD_CONCURRENCY. - События. Публикуется
pin.crm.lead-participants-changed.v1(ChangeType = Added) атомарно с мутацией состава: дельта добавленного участника + полный активный состав. Консюмеры:messenger(приглашает участника в чат лидаChatKind.Lead),notifications(уведомляет добавленного). - Результат. Актуальный состав лида → HTTP
201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | pin.crm.lead-participants-changed.v1 (ChangeType = Added) — контракт lead-participants-changed.md |
| Внешние вызовы | none (пользователь проверяется по логической ссылке identity, без синхронного запроса) |
| Проекции / поиск | обновление членства → пересчёт видимости лида (доступ добавленного участника) |
| Уведомления | notifications уведомляет добавленного участника (по интеграционному событию) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | LeadId/UserId = Guid.Empty, неизвестное значение Role | «Проверьте корректность полей» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден по LeadId в tenant | «Лид не найден» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | Status != Active (INV-LEAD-2) | «Менять состав можно только у активного лида» | no retry |
CRM_LEAD_FORBIDDEN | 403 | актор не участник агентской роли / не руководитель / не Менеджер ПИН | «Недостаточно прав на изменение состава лида» | no retry |
CRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED | 422 | роль Executor/InvestorPrimary (сингулярная ключевая) или SellManager/PinManager (неприменима к лиду) | «Эту роль нельзя добавить в состав лида» | no retry |
CRM_LEAD_PARTICIPANT_DUPLICATE | 409 | UserId уже активный участник лида (INV-LEAD-7) | «Пользователь уже участник лида» | no retry |
CRM_LEAD_PARTICIPANT_ROLE_CONFLICT | 409 | добавляется второй Responsible при существующем активном (INV-LEAD-7) | «Куратор уже назначен» | no retry |
CRM_LEAD_CONCURRENCY | 409 | конфликт RowVersion (параллельная смена состава/передача ведения) | «Лид изменён, обновите и повторите» | retry (перечитать) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор авторизован, имеет permission
crm.leads.manage, является участником агентской роли лида / руководителем агентства / Менеджером ПИН; - лид существует в
TenantIdактора и находится в статусеActive(INV-LEAD-2); - добавляемый
UserId— зарегистрированный (в т.ч. lazy) пользовательidentity, ещё не активный участник этого лида.
Постусловия (гарантированы после успеха):
- в
crm.lead_participantsпоявилась активная строка{UserId, Role, IsPrimary = false, RemovedAt = null}; - при
Role = ResponsibleпроизводнаяResponsibleAgentUserId= добавленный участник; иные производные лида (ExecutorAgentUserId,PrimaryInvestorUserId) не изменены; Versionлида инкрементирован, статус остаётсяActive;- опубликован
pin.crm.lead-participants-changed.v1(Added) с полным активным составом;messengerдобавляет участника в чат лида,notificationsуведомляет его; - заявки на фиксацию и сделки лида не изменены; Origin-атрибуция (
TenantId/OriginAgentUserId) не тронута.
Acceptance Criteria
- Happy path (со-агент). Дано: активный лид, актор — исполнитель. Когда: добавляет
UserIdс рольюCoAgent. Тогда:201, участник активен,Version+1, опубликованlead-participants-changed(Added), участник получил доступ к лиду и его чату. - Куратор при отсутствии. Дано: у лида нет активного
Responsible. Когда: добавляютResponsible. Тогда:201,ResponsibleAgentUserId= добавленный. - Куратор уже есть. Дано: активный
Responsibleуже назначен. Когда: добавляют второгоResponsible. Тогда:409 CRM_LEAD_PARTICIPANT_ROLE_CONFLICT, состав не меняется. - Дубль участника. Дано:
UserIdуже активный участник. Когда: повторное добавление. Тогда:409 CRM_LEAD_PARTICIPANT_DUPLICATE. - Запрещённая роль. Дано: попытка добавить
ExecutorилиInvestorPrimary. Когда: отправка. Тогда:422 CRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED(исполнитель — через передачу ведения; основной инвестор — через set-primary-investor.md). - Чужой лид. Дано: актор не участник агентской роли и не руководитель/Менеджер ПИН. Когда: добавление.
Тогда:
403 CRM_LEAD_FORBIDDEN. - Лид не активен. Дано: лид
Expired/Archived. Когда: добавление. Тогда:409 CRM_LEAD_NOT_ACTIVE. - Конкурентная смена состава. Дано: состав параллельно изменён. Когда: отправка с устаревшей версией.
Тогда:
409 CRM_LEAD_CONCURRENCY; клиент перечитывает и повторяет.
Совместимость и наблюдаемость
ParticipantRole— additive: новое значение роли у потребителя события трактуется строкой + warning (не падение). Набор ролей, допустимых к добавлению в лид, расширяется additive; сужение — breaking.- Logs:
lead-participant added(LeadId, tenant, actor,Role;UserId— только id); metrics:crm_lead_participant_add_total{result,role}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): DELETE /api/agent/crm/leads/{leadId}/participants/{userId} — Снять участника лида, POST /api/agent/crm/leads/{leadId}/primary-investor — Назначить основного инвестора лида
- Use Cases (3): UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора, UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора), US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность-дом: lead.md (
LeadParticipantEntity,ParticipantRole, INV-LEAD-2/7/8). - Смежные операции состава: remove-lead-participant.md, set-primary-investor.md.
- Событие: lead-participants-changed.md; аналог на сделке — deal.md (единая модель участников).
- ADR-0071 (владение процессами,
ParticipantRole, видимость по составу), ADR-0025 (scope-иерархия), ADR-0055 (мессенджер, чат лида).