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
| Поле | Значение |
|---|---|
| ID | UC-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 |
Основной Поток
- Контекст и доступ. Руководитель агентства-владельца (или текущий
Executor) открывает карточку нетерминального процесса и выбирает агента-куратора. - Ввод. Запрос несёт
ResponsibleUserId(кандидат в кураторы;null— снятие), опциональныеReason(≤ 1000) и наблюдаемую версию карточки для оптимистической блокировки. - Проверка входа. Идентификатор процесса задан; при назначении
ResponsibleUserId != пусто;Reasonв пределах длины — иначеVALIDATION_FAILED. - Проверки состояния и доступа.
- Процесс существует и принадлежит агентству-владельцу актора — иначе
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.
- Процесс существует и принадлежит агентству-владельцу актора — иначе
- Отсев no-op. Назначение уже действующего куратора либо снятие при отсутствующем
Responsible— no-op: мутаций и событий нет, возвращается текущая карточка. - Доменное решение (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) — не пересчитываются.
- назначение: прежний активный
- Что меняется. Состав участников (назначение/снятие
Responsible) и денормализованныйResponsibleAgentUserId; для сделки — строки таймлайнаMemberAdded/MemberRemoved(рольResponsible). Всё — в одном согласованном изменении вместе с записью в outbox. - Публикация. Интеграционное событие о смене состава (
ChangeType = ResponsibleChanged) с полным активным составом и дельтой публикуется через outbox: сделка —pin.crm.deal-members-changed.v1; лид —pin.crm.lead-participants-changed.v1. - Результат. Возвращается карточка процесса с новым
ResponsibleAgentUserId(илиnull), полным активным составом, свежей версией. Куратор виден на карточке сразу. - Асинхронно.
messengerреконсилирует участников чата процесса (куратор приглашён/снят),notificationsуведомляет назначенного/снятого куратора (лаг < 1 мин; порядок изменений per-process — по ключуDealId/LeadId).
Диаграмма
Альтернативные Потоки
A1. Ответственный = исполнитель (совпадение допускается)
Заголовок use case «ответственный ≠ исполнитель» указывает на различие ролей: куратор не является исполнителем сделки и не получает комиссию за неё. Запрета одному и тому же пользователю занимать обе роли одновременно — нет.
- Кандидат
ResponsibleUserIdсовпадает с текущимExecutor. - Разрешено: тот же пользователь получает вторую роль
Responsible(при завершении сделки/начислении комиссию получает какExecutor; рольResponsibleнефинансовая). Возврат200.
Сходится с основным потоком на шаге 8.
A2. Снятие куратора
- Актор отправляет
ResponsibleUserId = nullпри существующем активномResponsible. - Активный
Responsibleснимается из состава,ResponsibleAgentUserId = null; публикуется событиеResponsibleChanged(дельтаRemoved).
Возврат 200 с составом без куратора.
A3. Кандидат не из агентства-владельца
- Проверка членства находит: кандидат — агент другого агентства либо без активного членства.
CRM_PARTICIPANT_NOT_IN_AGENCY(422); состав не меняется.
Возврат: выбрать кандидата из активных агентов агентства-владельца и повторить.
A4. Терминальная сделка / неактивный лид
- Проверка состояния находит сделку
Completed/Cancelled(→CRM_DEAL_ALREADY_TERMINAL) либо лидExpired/Archived(→CRM_LEAD_NOT_ACTIVE). - Куратор не меняется.
A5. Конкурентная правка состава
- Параллельная смена состава/передача ведения изменила версию карточки.
- Проигравший коммит получает
CRM_DEAL_CONFLICT/CRM_LEAD_CONFLICT(409).
Возврат: перечитать карточку и повторить.
Повторное назначение уже действующего куратора или снятие при отсутствующем 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_FAILED | 400 | Формат идентификатора процесса/ResponsibleUserId/Reason | Показать ошибки запроса |
CRM_DEAL_NOT_FOUND / CRM_LEAD_NOT_FOUND | 404 | Процесса нет или он чужого агентства | Сообщить, что процесс не найден |
CRM_DEAL_FORBIDDEN / CRM_LEAD_FORBIDDEN | 403 | Актор — не руководитель/админ и не текущий Executor | Скрыть действие «назначить куратора» |
CRM_PARTICIPANT_NOT_IN_AGENCY | 422 | Кандидат — не активный агент агентства-владельца | Предложить выбрать агента своего агентства |
CRM_DEAL_ALREADY_TERMINAL | 409 | Сделка Completed/Cancelled (INV-DEAL-9) | Сообщить, что сделка завершена |
CRM_LEAD_NOT_ACTIVE | 409 | Лид Expired/Archived (INV-LEAD-2) | Сообщить, что лид неактивен |
CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT | 409 | Конкурентная правка состава / устаревшая версия | Перечитать карточку и повторить |
CRM_DEAL_NOT_FOUND / CRM_LEAD_NOT_FOUND возвращается одинаково и для отсутствующего процесса, и для
процесса другого агентства — существование чужой записи клиенту не раскрывается.
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
LeadParticipantsChanged / DealMembersChanged | Назначение/снятие Responsible (не в no-op) | Внутренний диспетчер → сборка интеграционного события; для сделки — запись таймлайна MemberAdded/MemberRemoved |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DealMembersChangedEvent | pin.crm.deal-members-changed.v1 | Смена куратора сделки (ChangeType = ResponsibleChanged), через outbox атомарно | messenger (реконсиляция участников чата сделки), notifications — контракт ../events/deal-members-changed.md |
LeadParticipantsChangedEvent | pin.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 + консюмер-контракт |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора, UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: ../api/agent/reassign-responsible.md; смежная операция — ../api/agent/reassign-executor.md.
- Domain entity: ../domain/lead.md (INV-LEAD-7,
ParticipantRole), ../domain/deal.md (INV-DEAL-3), таймлайн — ../domain/deal-timeline-event.md. - Kafka события: ../events/deal-members-changed.md, ../events/lead-participants-changed.md.
- Членство агента: ../../identity/domain/agent-membership.md.
- Смежные UC: UC-CRM-011-lead-multiple-investors-set-primary.md, UC-CRM-012-lead-participants.md; US-реестр — user-stories-deal.md.
- ADR: 0025 scoped-rbac, 0052 tenancy, 0071 crm-ownership-and-participant-model.