UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора)
Назначение
Описывает сценарий расширения и сужения команды ведения лида: агент добавляет или снимает участников
состава (LeadParticipantEntity, единый ParticipantRole, ADR-0071) — со-агента (второй агент), ассистента,
юриста сопровождения, куратора (Responsible) и созаёмщика/соинвестора (InvestorSecondary — например, супруг
инвестора). Видимость лида и синк участников чата лида (ChatKind.Lead, messenger) строятся на этом составе,
поэтому добавление открывает участнику доступ к лиду и его чату, а снятие — закрывает (история переписки
сохраняется). Связан с контекстом crm. Дом полей — ../domain/lead.md (раздел «Участники
лида»). Сингулярные ключевые роли здесь не заводятся: исполнитель — только передачей ведения
(../api/agent/reassign-executor.md); основной инвестор — только
UC-CRM-011.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-012 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент-исполнитель / куратор лида (агентская роль в составе) / администратор-руководитель агентства / Менеджер ПИН |
| Permissions / scope | crm.leads.manage; scope — организация (агентство), record-level по членству (ADR-0071); руководитель — весь TenantId (ADR-0025); Менеджер ПИН — платформа |
| Tenant | TenantId = AgencyCompanyId; lead.TenantId обязан совпасть |
| Record-level | доступ = актор ∈ активные участники лида агентской роли (Executor/Responsible) или руководитель компании-владельца или Менеджер ПИН |
Предусловие
- Лид существует в
TenantIdактора иStatus = Active(INV-LEAD-2). - Актор имеет доступ по членству/руководству/платформе (record-level, ADR-0071).
- Добавление:
UserId— зарегистрированный (в т.ч. lazy) пользовательidentity, ещё не активный участник этого лида; роль — из применимых к лиду (CoAgent/Assistant/Lawyer/Responsible/InvestorSecondary). - Снятие:
UserId— активный участник лида, не единственныйExecutorи неInvestorPrimary.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI-действие → вызов API |
| Триггерящий элемент | «Добавить участника» → POST /api/agent/crm/leads/{leadId}/participants; «Убрать участника» → DELETE /api/agent/crm/leads/{leadId}/participants/{userId} |
Основной Поток
Основная нить — добавление участника (со-агент / юрист / супруг-соинвестор). Снятие участника — в альтернативе A5; проверки доступа, состояния лида, атомарность и публикация события общие для обеих операций.
- Контекст и доступ. Агент открывает карточку активного лида и выбирает пользователя и роль состава
(например, второго агента —
CoAgent; юриста инвестора —Lawyer; супруга —InvestorSecondary). - Ввод. Запрос несёт
UserIdдобавляемого и егоRole. - Проверка входа.
LeadId/UserIdзаданы;Role— известное значение, применимое к лиду — иначеVALIDATION_FAILED. - Проверки состояния и доступа.
- Лид существует в
TenantId— иначеCRM_LEAD_NOT_FOUND. Status = Active(INV-LEAD-2) — иначеCRM_LEAD_NOT_ACTIVE.- Record-level (членство/руководство/Менеджер ПИН) — иначе
CRM_LEAD_FORBIDDEN. - Роль допустима к добавлению:
Executor/InvestorPrimary(сингулярные ключевые) не заводятся здесь;SellManager/PinManagerнеприменимы к лиду — иначеCRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED.
- Лид существует в
- Проверка состава (INV-LEAD-7).
UserIdне должен уже быть активным участником — иначеCRM_LEAD_PARTICIPANT_DUPLICATE.- Для сингулярной роли
Responsible: если активный куратор уже есть —CRM_LEAD_PARTICIPANT_ROLE_CONFLICT(сменить куратора — снять текущего и добавить нового).
- Доменное решение. Добавляется активное участие
{ UserId, Role, IsPrimary = false }; строка лида блокируется от гонки со сменой состава/передачей ведения. Производные лида не меняются, кромеResponsibleAgentUserId(приRole = Responsible— назначается добавленным). Статус лидаActive → Active; заявки на фиксацию и сделки лида не затрагиваются. - Что меняется. Появляется активная строка состава, обновляются версия лида (и
ResponsibleAgentUserIdпри добавлении куратора) — в одном согласованном изменении вместе с записью в outbox. - Публикация. Интеграционное событие
pin.crm.lead-participants-changed.v1(ChangeType = Added) с дельтой добавленного участника и полным активным составом. - Результат. Возвращается актуальный состав лида; добавленный участник получает доступ к лиду и его чату.
- Асинхронно.
messengerприглашает участника в чат лида (ChatKind.Lead);notificationsуведомляет добавленного (лаг < 1 мин; порядок изменений per-lead — по ключуLeadId).
Диаграмма
Альтернативные Потоки
A1. Второй агент (со-агент)
- Актор добавляет
UserIdс рольюCoAgent— второй агент подключается к ведению. - Со-агент получает доступ к лиду и чату (нефинансовая роль в v1, ADR-0069);
201.
A2. Юрист/ассистент инвестора
- Актор добавляет
UserIdс рольюLawyer(юрист сопровождения) илиAssistant. - Участник получает доступ к лиду и чату; производные лида не меняются;
201.
A3. Супруг/созаёмщик инвестора (InvestorSecondary)
- Актор добавляет
UserIdс рольюInvestorSecondary— супруг/партнёр инвестора. - Соинвестор добавлен в состав (
IsPrimary = false); основной инвестор не меняется. Чтобы сделать его основным — UC-CRM-011;201.
A4. Назначение/смена куратора (Responsible)
- Если активного
Responsibleнет — добавлениеResponsibleназначает куратора (ResponsibleAgentUserId= добавленный);201. - Если куратор уже есть —
CRM_LEAD_PARTICIPANT_ROLE_CONFLICT(409); сменить куратора можно снятием текущего и добавлением нового, либо операцией назначения ответственного (UC-CRM-010).
A5. Снятие участника (DELETE)
- Актор снимает участника (
UserId— активный участник лида). - Guard ключевых ролей (INV-LEAD-7/8): снятие единственного активного
Executor→CRM_LEAD_EXECUTOR_REQUIRED(замена — только атомарной передачей ведения); снятие активногоInvestorPrimary→CRM_PRIMARY_INVESTOR_REQUIRED(сначала назначить нового основного, UC-CRM-011). - Иначе участие переводится в снятое (
RemovedAt = now, soft-remove; строка остаётся историей чата/аудита); при снятии активногоResponsible—ResponsibleAgentUserId = null. - Публикуется
pin.crm.lead-participants-changed.v1(ChangeType = Removed) с полным составом;messengerснимает доступ снятого (история ему сохраняется),notificationsуведомляет о смене состава. Возврат200. - Повтор снятия для уже снятого/несуществующего участника →
CRM_LEAD_PARTICIPANT_NOT_FOUND(404, идемпотентностьDELETE).
A6. Лид неактивен
- Лид
Expired/Archived. CRM_LEAD_NOT_ACTIVE(409); состав не меняется. Возврат: продлить/реактивировать лид (INV-LEAD-3), затем повтор.
A7. Конкурентная смена состава
- Параллельная смена состава/передача ведения изменила версию лида.
CRM_LEAD_CONCURRENCY(409). Возврат: перечитать лид и повторить.
Постусловие
- Добавление: в составе появилась активная строка
{ UserId, Role, IsPrimary = false }; приRole = Responsible—ResponsibleAgentUserId= добавленный; иные производные (ExecutorAgentUserId,PrimaryInvestorUserId) не изменены; версия лида +1; статусActive. - Снятие: участие переведено в снятое (
RemovedAt != null), из активного состава исчезло, строка сохранена как история; при снятии куратора —ResponsibleAgentUserId = null; версия лида +1. - Ключевые инварианты состава сохранены: ровно один активный
Executorи одинInvestorPrimary(INV-LEAD-7/8);UserIdуникален среди активных. - Заявки на фиксацию и сделки лида не изменены; Origin-атрибуция (
TenantId/OriginAgentUserId) не тронута. - Опубликован
pin.crm.lead-participants-changed.v1(Added/Removed) с полным активным составом.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_FAILED | 400 | LeadId/UserId пусто; неизвестное значение Role | Показать ошибки полей |
CRM_LEAD_NOT_FOUND | 404 | Лид не найден по LeadId в tenant | Сообщить, что лид не найден |
CRM_LEAD_PARTICIPANT_NOT_FOUND | 404 | При снятии — UserId не активный участник (нет/уже снят) | Сообщить, что участник не найден (идемпотентность DELETE) |
CRM_LEAD_NOT_ACTIVE | 409 | Лид Expired/Archived (INV-LEAD-2) | Предложить продлить/реактивировать лид |
CRM_LEAD_FORBIDDEN | 403 | Актор не участник агентской роли / не руководитель / не Менеджер ПИН | Скрыть действия над составом |
CRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED | 422 | Роль Executor/InvestorPrimary (сингулярная ключевая) или SellManager/PinManager (неприменима к лиду) | Исполнитель — через передачу ведения; основной инвестор — через UC-CRM-011 |
CRM_LEAD_PARTICIPANT_DUPLICATE | 409 | UserId уже активный участник (INV-LEAD-7) | Сообщить, что пользователь уже в составе |
CRM_LEAD_PARTICIPANT_ROLE_CONFLICT | 409 | Добавляется второй Responsible при существующем (INV-LEAD-7) | Сообщить, что куратор уже назначен |
CRM_LEAD_EXECUTOR_REQUIRED | 409 | Снятие единственного активного Executor (INV-LEAD-8) | Предложить передачу ведения вместо снятия |
CRM_PRIMARY_INVESTOR_REQUIRED | 409 | Снятие активного InvestorPrimary (INV-LEAD-7) | Предложить сначала назначить другого основного |
CRM_LEAD_CONCURRENCY | 409 | Конфликт версии (параллельная смена состава/передача ведения) | Перечитать и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
LeadParticipantsChanged | Добавление / снятие участника | Внутренний диспетчер → сборка LeadParticipantsChangedEvent |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
LeadParticipantsChangedEvent | pin.crm.lead-participants-changed.v1 | Добавление (ChangeType = Added) / снятие (ChangeType = Removed) участника, через outbox атомарно | messenger (пригласить/убрать участника в чат лида ChatKind.Lead), notifications — контракт ../events/lead-participants-changed.md |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Добавление CoAgent/Lawyer/InvestorSecondary → 201, участник активен, версия +1, событие Added, доступ к лиду и чату открыт | Тест POST .../participants по каждой роли |
| AC2 | Добавление Responsible при отсутствии куратора → 201, ResponsibleAgentUserId = добавленный | Положительный тест |
| AC3 | Второй Responsible при существующем → CRM_LEAD_PARTICIPANT_ROLE_CONFLICT (409), состав без изменений | Негативный тест |
| AC4 | Дубль участника → CRM_LEAD_PARTICIPANT_DUPLICATE; попытка Executor/InvestorPrimary/SellManager/PinManager → CRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED | Негативные тесты состава/роли |
| AC5 | Снятие со-агента → 200, участие в снятом состоянии, событие Removed, доступ снят; повтор снятия → CRM_LEAD_PARTICIPANT_NOT_FOUND | Тест снятия + идемпотентность |
| AC6 | Снятие единственного Executor → CRM_LEAD_EXECUTOR_REQUIRED; снятие InvestorPrimary → CRM_PRIMARY_INVESTOR_REQUIRED; состав без изменений | Негативные тесты guard ключевых ролей |
| AC7 | Лид Expired/Archived → CRM_LEAD_NOT_ACTIVE; чужой актор → CRM_LEAD_FORBIDDEN | Негативные тесты состояния и доступа |
| AC8 | Успех публикует ровно одно событие с полным составом; messenger синхронизирует чат за ≤ 1 мин; Origin-атрибуция не тронута | Тест outbox + консюмер-контракт |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель, UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: ../api/agent/add-lead-participant.md, ../api/agent/remove-lead-participant.md.
- Domain entity: ../domain/lead.md (
LeadParticipantEntity,ParticipantRole, INV-LEAD-2/7/8). - Kafka событие: ../events/lead-participants-changed.md.
- Смежные UC: UC-CRM-011-lead-multiple-investors-set-primary.md (назначение основного инвестора), UC-CRM-010-reassign-responsible.md (назначение куратора); передача ведения — ../api/agent/reassign-executor.md.
- US-реестр контекста: user-stories-deal.md.
- ADR: 0071 crm-ownership-and-participant-model, 0072 agent-termination-cascade (каскад увольнения — массовый soft-remove участий), 0055 own-mini-messenger (чат лида), 0025 scoped-rbac.