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

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

ПолеЗначение
IDUC-CRM-012
Контекстcrm (см. docs/06-services/crm/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент-исполнитель / куратор лида (агентская роль в составе) / администратор-руководитель агентства / Менеджер ПИН
Permissions / scopecrm.leads.manage; scope — организация (агентство), record-level по членству (ADR-0071); руководитель — весь TenantId (ADR-0025); Менеджер ПИН — платформа
TenantTenantId = 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; проверки доступа, состояния лида, атомарность и публикация события общие для обеих операций.

  1. Контекст и доступ. Агент открывает карточку активного лида и выбирает пользователя и роль состава (например, второго агента — CoAgent; юриста инвестора — Lawyer; супруга — InvestorSecondary).
  2. Ввод. Запрос несёт UserId добавляемого и его Role.
  3. Проверка входа. LeadId/UserId заданы; Role — известное значение, применимое к лиду — иначе VALIDATION_FAILED.
  4. Проверки состояния и доступа.
    • Лид существует в 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.
  5. Проверка состава (INV-LEAD-7).
    • UserId не должен уже быть активным участником — иначе CRM_LEAD_PARTICIPANT_DUPLICATE.
    • Для сингулярной роли Responsible: если активный куратор уже есть — CRM_LEAD_PARTICIPANT_ROLE_CONFLICT (сменить куратора — снять текущего и добавить нового).
  6. Доменное решение. Добавляется активное участие { UserId, Role, IsPrimary = false }; строка лида блокируется от гонки со сменой состава/передачей ведения. Производные лида не меняются, кроме ResponsibleAgentUserId (при Role = Responsible — назначается добавленным). Статус лида Active → Active; заявки на фиксацию и сделки лида не затрагиваются.
  7. Что меняется. Появляется активная строка состава, обновляются версия лида (и ResponsibleAgentUserId при добавлении куратора) — в одном согласованном изменении вместе с записью в outbox.
  8. Публикация. Интеграционное событие pin.crm.lead-participants-changed.v1 (ChangeType = Added) с дельтой добавленного участника и полным активным составом.
  9. Результат. Возвращается актуальный состав лида; добавленный участник получает доступ к лиду и его чату.
  10. Асинхронно. messenger приглашает участника в чат лида (ChatKind.Lead); notifications уведомляет добавленного (лаг < 1 мин; порядок изменений per-lead — по ключу LeadId).

Диаграмма

Альтернативные Потоки

A1. Второй агент (со-агент)

  1. Актор добавляет UserId с ролью CoAgent — второй агент подключается к ведению.
  2. Со-агент получает доступ к лиду и чату (нефинансовая роль в v1, ADR-0069); 201.

A2. Юрист/ассистент инвестора

  1. Актор добавляет UserId с ролью Lawyer (юрист сопровождения) или Assistant.
  2. Участник получает доступ к лиду и чату; производные лида не меняются; 201.

A3. Супруг/созаёмщик инвестора (InvestorSecondary)

  1. Актор добавляет UserId с ролью InvestorSecondary — супруг/партнёр инвестора.
  2. Соинвестор добавлен в состав (IsPrimary = false); основной инвестор не меняется. Чтобы сделать его основным — UC-CRM-011; 201.

A4. Назначение/смена куратора (Responsible)

  1. Если активного Responsible нет — добавление Responsible назначает куратора (ResponsibleAgentUserId = добавленный); 201.
  2. Если куратор уже есть — CRM_LEAD_PARTICIPANT_ROLE_CONFLICT (409); сменить куратора можно снятием текущего и добавлением нового, либо операцией назначения ответственного (UC-CRM-010).

A5. Снятие участника (DELETE)

  1. Актор снимает участника (UserId — активный участник лида).
  2. Guard ключевых ролей (INV-LEAD-7/8): снятие единственного активного ExecutorCRM_LEAD_EXECUTOR_REQUIRED (замена — только атомарной передачей ведения); снятие активного InvestorPrimaryCRM_PRIMARY_INVESTOR_REQUIRED (сначала назначить нового основного, UC-CRM-011).
  3. Иначе участие переводится в снятое (RemovedAt = now, soft-remove; строка остаётся историей чата/аудита); при снятии активного ResponsibleResponsibleAgentUserId = null.
  4. Публикуется pin.crm.lead-participants-changed.v1 (ChangeType = Removed) с полным составом; messenger снимает доступ снятого (история ему сохраняется), notifications уведомляет о смене состава. Возврат 200.
  5. Повтор снятия для уже снятого/несуществующего участника → CRM_LEAD_PARTICIPANT_NOT_FOUND (404, идемпотентность DELETE).

A6. Лид неактивен

Лид неактивен
  1. Лид Expired/Archived.
  2. CRM_LEAD_NOT_ACTIVE (409); состав не меняется. Возврат: продлить/реактивировать лид (INV-LEAD-3), затем повтор.

A7. Конкурентная смена состава

Конкурентная смена состава
  1. Параллельная смена состава/передача ведения изменила версию лида.
  2. CRM_LEAD_CONCURRENCY (409). Возврат: перечитать лид и повторить.

Постусловие

  • Добавление: в составе появилась активная строка { UserId, Role, IsPrimary = false }; при Role = ResponsibleResponsibleAgentUserId = добавленный; иные производные (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_FAILED400LeadId/UserId пусто; неизвестное значение RoleПоказать ошибки полей
CRM_LEAD_NOT_FOUND404Лид не найден по LeadId в tenantСообщить, что лид не найден
CRM_LEAD_PARTICIPANT_NOT_FOUND404При снятии — UserId не активный участник (нет/уже снят)Сообщить, что участник не найден (идемпотентность DELETE)
CRM_LEAD_NOT_ACTIVE409Лид Expired/Archived (INV-LEAD-2)Предложить продлить/реактивировать лид
CRM_LEAD_FORBIDDEN403Актор не участник агентской роли / не руководитель / не Менеджер ПИНСкрыть действия над составом
CRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWED422Роль Executor/InvestorPrimary (сингулярная ключевая) или SellManager/PinManager (неприменима к лиду)Исполнитель — через передачу ведения; основной инвестор — через UC-CRM-011
CRM_LEAD_PARTICIPANT_DUPLICATE409UserId уже активный участник (INV-LEAD-7)Сообщить, что пользователь уже в составе
CRM_LEAD_PARTICIPANT_ROLE_CONFLICT409Добавляется второй Responsible при существующем (INV-LEAD-7)Сообщить, что куратор уже назначен
CRM_LEAD_EXECUTOR_REQUIRED409Снятие единственного активного Executor (INV-LEAD-8)Предложить передачу ведения вместо снятия
CRM_PRIMARY_INVESTOR_REQUIRED409Снятие активного InvestorPrimary (INV-LEAD-7)Предложить сначала назначить другого основного
CRM_LEAD_CONCURRENCY409Конфликт версии (параллельная смена состава/передача ведения)Перечитать и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
LeadParticipantsChangedДобавление / снятие участникаВнутренний диспетчер → сборка LeadParticipantsChangedEvent

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
LeadParticipantsChangedEventpin.crm.lead-participants-changed.v1Добавление (ChangeType = Added) / снятие (ChangeType = Removed) участника, через outbox атомарноmessenger (пригласить/убрать участника в чат лида ChatKind.Lead), notifications — контракт ../events/lead-participants-changed.md

Acceptance Criteria

#КритерийПроверка
AC1Добавление CoAgent/Lawyer/InvestorSecondary201, участник активен, версия +1, событие Added, доступ к лиду и чату открытТест POST .../participants по каждой роли
AC2Добавление Responsible при отсутствии куратора → 201, ResponsibleAgentUserId = добавленныйПоложительный тест
AC3Второй Responsible при существующем → CRM_LEAD_PARTICIPANT_ROLE_CONFLICT (409), состав без измененийНегативный тест
AC4Дубль участника → CRM_LEAD_PARTICIPANT_DUPLICATE; попытка Executor/InvestorPrimary/SellManager/PinManagerCRM_LEAD_PARTICIPANT_ROLE_NOT_ALLOWEDНегативные тесты состава/роли
AC5Снятие со-агента → 200, участие в снятом состоянии, событие Removed, доступ снят; повтор снятия → CRM_LEAD_PARTICIPANT_NOT_FOUNDТест снятия + идемпотентность
AC6Снятие единственного ExecutorCRM_LEAD_EXECUTOR_REQUIRED; снятие InvestorPrimaryCRM_PRIMARY_INVESTOR_REQUIRED; состав без измененийНегативные тесты guard ключевых ролей
AC7Лид Expired/ArchivedCRM_LEAD_NOT_ACTIVE; чужой актор → CRM_LEAD_FORBIDDENНегативные тесты состояния и доступа
AC8Успех публикует ровно одно событие с полным составом; messenger синхронизирует чат за ≤ 1 мин; Origin-атрибуция не тронутаТест outbox + консюмер-контракт

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

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

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