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

UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель

Руководитель агентства-владельца (или текущий исполнитель) на карточке лида или сделки назначает либо снимает ответственного-куратора — нефинансовую роль наблюдения, не затрагивающую исполнителя, комиссию и Origin-атрибуцию.

Назначение

Описывает сценарий, в котором агентство закрепляет за лидом или сделкой ответственного (Responsible) — куратора ведения (руководитель группы, наставник), который видит процесс и подсвечивается в его чате, но не является исполнителем и не получает комиссию. Ответственный опционален и может как совпадать, так и не совпадать с исполнителем (Executor). Связан с контекстом crm; операция едина для лида (../domain/lead.md, INV-LEAD-7) и сделки (../domain/deal.md, INV-DEAL-3) — меняется только носитель роли Responsible, исполнитель и Origin-атрибуция не затрагиваются. Смежная операция «сменить исполнителя/получателя комиссии» — [UC не входит в этот файл] (../api/agent/reassign-executor.md).

Metadata

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

Актор

ПолеЗначение
РольРуководитель/администратор агентства-владельца или текущий активный Executor процесса
Permissions / scopeсделка — crm.deals.manage; лид — crm.leads.manage; scope — организация (агентство-владелец), record-level по членству (ADR-0071)
Tenantлид — TenantId = AgencyCompanyId; сделка — TenantId = DeveloperCompanyId (ADR-0052); агентский доступ — по предикату агентства-владельца + членству
Record-levelдоступ = руководитель/админ агентства-владельца или текущий Executor; рядовой со-агент/ассистент куратора не назначает

Предусловие

  • Процесс существует и принадлежит агентству-владельцу актора.
  • Процесс нетерминален: сделка Status ∉ {Completed, Cancelled} (INV-DEAL-9); лид Status = Active (INV-LEAD-2).
  • При назначении: кандидат ResponsibleUserId — активный агент агентства-владельца (AgentMembership.Status = Active, ../../identity/domain/agent-membership.md).
  • Роль Responsible опциональна: активный Responsible — не более одного.

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API
Триггерящий элементКнопка «Назначить куратора» / «Снять куратора» на карточке лида или сделки → PUT /api/agent/crm/{deals|leads}/{id}/responsible

Основной Поток

  1. Контекст и доступ. Руководитель агентства-владельца (или текущий Executor) открывает карточку нетерминального процесса и выбирает агента-куратора.
  2. Ввод. Запрос несёт ResponsibleUserId (кандидат в кураторы; null — снятие), опциональные Reason (≤ 1000) и наблюдаемую версию карточки для оптимистической блокировки.
  3. Проверка входа. Идентификатор процесса задан; при назначении ResponsibleUserId != пусто; Reason в пределах длины — иначе VALIDATION_FAILED.
  4. Проверки состояния и доступа.
    • Процесс существует и принадлежит агентству-владельцу актора — иначе CRM_DEAL_NOT_FOUND / CRM_LEAD_NOT_FOUND (существование чужих процессов не раскрывается).
    • Право актора: руководитель/админ агентства-владельца или текущий Executor — иначе CRM_DEAL_FORBIDDEN / CRM_LEAD_FORBIDDEN.
    • Состояние допускает правку состава: сделка нетерминальна — иначе CRM_DEAL_ALREADY_TERMINAL; лид активен — иначе CRM_LEAD_NOT_ACTIVE.
    • При назначении — кандидат активный агент агентства-владельца (проверка членства по реплике/справке identity) — иначе CRM_PARTICIPANT_NOT_IN_AGENCY.
  5. Отсев no-op. Назначение уже действующего куратора либо снятие при отсутствующем Responsible — no-op: мутаций и событий нет, возвращается текущая карточка.
  6. Доменное решение (INV-DEAL-3 / INV-LEAD-7; Responsible ≤ 1 активный). Атомарно:
    • назначение: прежний активный Responsible (если был) снимается; кандидат становится активным Responsible (если он уже участник иной нефинансовой роли — роль меняется на Responsible, иначе добавляется новым участником). Ограничения «≠ Executor» нет: куратор может совпадать с исполнителем;
    • снятие (ResponsibleUserId = null): активный Responsible снимается из состава; если тот же пользователь остаётся Executor — снимается только роль куратора, участие исполнителя сохраняется.
    • Executor / основной инвестор не трогаются; денормализованный ResponsibleAgentUserId обновляется (или null); Origin-поля иммутабельны (INV-DEAL-10 / INV-LEAD-6) — не пересчитываются.
  7. Что меняется. Состав участников (назначение/снятие Responsible) и денормализованный ResponsibleAgentUserId; для сделки — строки таймлайна MemberAdded/MemberRemoved (роль Responsible). Всё — в одном согласованном изменении вместе с записью в outbox.
  8. Публикация. Интеграционное событие о смене состава (ChangeType = ResponsibleChanged) с полным активным составом и дельтой публикуется через outbox: сделка — pin.crm.deal-members-changed.v1; лид — pin.crm.lead-participants-changed.v1.
  9. Результат. Возвращается карточка процесса с новым ResponsibleAgentUserId (или null), полным активным составом, свежей версией. Куратор виден на карточке сразу.
  10. Асинхронно. messenger реконсилирует участников чата процесса (куратор приглашён/снят), notifications уведомляет назначенного/снятого куратора (лаг < 1 мин; порядок изменений per-process — по ключу DealId/LeadId).

Диаграмма

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

A1. Ответственный = исполнитель (совпадение допускается)

Роль, а не запрет на пользователя

Заголовок use case «ответственный ≠ исполнитель» указывает на различие ролей: куратор не является исполнителем сделки и не получает комиссию за неё. Запрета одному и тому же пользователю занимать обе роли одновременно — нет.

  1. Кандидат ResponsibleUserId совпадает с текущим Executor.
  2. Разрешено: тот же пользователь получает вторую роль Responsible (при завершении сделки/начислении комиссию получает как Executor; роль Responsible нефинансовая). Возврат 200.

Сходится с основным потоком на шаге 8.

A2. Снятие куратора

  1. Актор отправляет ResponsibleUserId = null при существующем активном Responsible.
  2. Активный Responsible снимается из состава, ResponsibleAgentUserId = null; публикуется событие ResponsibleChanged (дельта Removed).

Возврат 200 с составом без куратора.

A3. Кандидат не из агентства-владельца

  1. Проверка членства находит: кандидат — агент другого агентства либо без активного членства.
  2. CRM_PARTICIPANT_NOT_IN_AGENCY (422); состав не меняется.

Возврат: выбрать кандидата из активных агентов агентства-владельца и повторить.

A4. Терминальная сделка / неактивный лид

  1. Проверка состояния находит сделку Completed/Cancelled (→ CRM_DEAL_ALREADY_TERMINAL) либо лид Expired/Archived (→ CRM_LEAD_NOT_ACTIVE).
  2. Куратор не меняется.

A5. Конкурентная правка состава

  1. Параллельная смена состава/передача ведения изменила версию карточки.
  2. Проигравший коммит получает CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT (409).

Возврат: перечитать карточку и повторить.

No-op идемпотентен

Повторное назначение уже действующего куратора или снятие при отсутствующем Responsible (шаг 5 основного потока) не создаёт мутаций и не публикует событие — клиент получает актуальную карточку без изменений.

Постусловие

  • Назначение: активный Responsible процесса — ResponsibleUserId (ровно один); прежний куратор снят; денормализованный ResponsibleAgentUserId обновлён; Executor и основной инвестор не изменены; версия +1.
  • Снятие: ResponsibleAgentUserId = null, активный Responsible снят; инвариант единственности Executor (INV-DEAL-3 / INV-LEAD-7/8) не нарушен.
  • No-op: состав, денормализация и outbox не изменены.
  • Origin-атрибуция (OriginAgentUserId, OriginAgencyCompanyId/TenantId) не изменена.
  • Опубликовано ровно одно событие смены состава (ChangeType = ResponsibleChanged) — кроме no-op.
  • Комиссия и её получатель не затронуты (роль Responsible нефинансовая).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400Формат идентификатора процесса/ResponsibleUserId/ReasonПоказать ошибки запроса
CRM_DEAL_NOT_FOUND / CRM_LEAD_NOT_FOUND404Процесса нет или он чужого агентстваСообщить, что процесс не найден
CRM_DEAL_FORBIDDEN / CRM_LEAD_FORBIDDEN403Актор — не руководитель/админ и не текущий ExecutorСкрыть действие «назначить куратора»
CRM_PARTICIPANT_NOT_IN_AGENCY422Кандидат — не активный агент агентства-владельцаПредложить выбрать агента своего агентства
CRM_DEAL_ALREADY_TERMINAL409Сделка Completed/Cancelled (INV-DEAL-9)Сообщить, что сделка завершена
CRM_LEAD_NOT_ACTIVE409Лид Expired/Archived (INV-LEAD-2)Сообщить, что лид неактивен
CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT409Конкурентная правка состава / устаревшая версияПеречитать карточку и повторить
Чужие процессы не раскрываются

CRM_DEAL_NOT_FOUND / CRM_LEAD_NOT_FOUND возвращается одинаково и для отсутствующего процесса, и для процесса другого агентства — существование чужой записи клиенту не раскрывается.

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

СобытиеКогда поднимаетсяSubscribers
LeadParticipantsChanged / DealMembersChangedНазначение/снятие Responsible (не в no-op)Внутренний диспетчер → сборка интеграционного события; для сделки — запись таймлайна MemberAdded/MemberRemoved

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

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

Acceptance Criteria

#КритерийПроверка
AC1Руководитель назначает куратором активного агента своего агентства (≠ Executor) → активный Responsible = кандидат, Executor не изменён, версия +1Тест PUT .../responsible → 200; проверить состав и ResponsibleAgentUserId
AC2Куратор совпадает с Executor — разрешено (ограничения «≠ Executor» нет)Тест назначения тем же пользователем → 200; один участник в двух ролях
AC3Снятие (ResponsibleUserId = null) → ResponsibleAgentUserId = null, Executor не затронут, событие ResponsibleChanged (дельта Removed)Негативно-положительный тест снятия
AC4Кандидат из другого агентства (или без активного членства) → CRM_PARTICIPANT_NOT_IN_AGENCY (422), состав без измененийНегативный тест
AC5Назначение уже действующего куратора / снятие при отсутствующем → 200 без мутаций и событийТест идемпотентного no-op
AC6Терминальная сделка → CRM_DEAL_ALREADY_TERMINAL; неактивный лид → CRM_LEAD_NOT_ACTIVEНегативные тесты состояния
AC7Рядовой со-агент (не руководитель, не Executor) → CRM_*_FORBIDDEN (403)Негативный тест доступа
AC8Успех публикует ровно одно событие смены состава (ResponsibleChanged) с полным составом; messenger реконсилирует чат за ≤ 1 минТест outbox + консюмер-контракт

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

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

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