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

UC-CRM-009. Передача ведения сделки (смена исполнителя)

Назначение

Описывает сценарий, в котором руководитель/администратор агентства-владельца (или сам текущий исполнитель — передавая ведение другому агенту) назначает по сделке нового исполнителя (Executor) из активных агентов своего агентства. Исполнитель — единственный получатель комиссии, поэтому передача ведения одновременно переносит будущего получателя вознаграждения. Смена происходит атомарно: инвариант «ровно один активный Executor» соблюдается в той же операции — сделка ни на миг не остаётся без исполнителя. Историческая Origin-атрибуция («кто привёл клиента») иммутабельна (INV-DEAL-10) и не пересчитывается. После фиксации получателя комиссии (завершение сделки + подтверждение начисления в rewards) сменить получателя передачей ведения нельзя. Агрегат — DealEntity (INV-DEAL-3, INV-DEAL-10). Связан с контекстом crm.

Metadata

ПолеЗначение
IDUC-CRM-009
Контекстcrm (см. docs/06-services/crm/)
Типосновной (+ альтернативные потоки A1–A5)
Статусdraft

Актор

ПолеЗначение
РольРуководитель/администратор агентства-владельца или текущий активный Executor сделки (передача «от себя» другому агенту)
Permissions / scopecrm.deals.manage — организация (агентство-владелец): scope-иерархия ADR-0025
TenantСделка TenantId = DeveloperCompanyId (ADR-0052); агентский доступ — по предикату агентства-владельца + членству, а не по авто-tenant-фильтру
Record-levelруководитель/админ агентства-владельца (агентство активного Executor, в v1 = OriginAgencyCompanyId) или сам текущий Executor; рядовой со-агент/ассистент ведение не передаёт

Предусловие

  • Актор аутентифицирован и несёт crm.deals.manage; он — руководитель/админ агентства-владельца или текущий активный Executor сделки.
  • Сделка существует (DealEntity), принадлежит агентству-владельцу актора и нетерминальна (Status ∉ {Completed, Cancelled}, INV-DEAL-9).
  • Кандидат NewExecutorUserIdактивный агент агентства-владельца сделки (agent-membership.md, Status = Active).
  • Кандидат != текущему исполнителю (иначе — no-op).

Триггер

ПолеЗначение
ИсточникUI-действие / вызов API
Триггерящий элементКарточка сделки, «Передать ведение»; PUT /api/agent/crm/deals/{dealId}/executor (reassign-executor.md)

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

  1. Актор открывает карточку сделки и выбирает «Передать ведение»: указывает нового исполнителя (NewExecutorUserId), опционально — оставить ли прежнего исполнителя в составе со-агентом (DemotePriorToCoAgent), причину (Reason) и наблюдаемую версию карточки для оптимистической блокировки.
  2. Интерфейс отправляет запрос смены исполнителя в маршруте сделки.
  3. Система определяет доступ: crm.deals.manage; актор — руководитель/админ агентства-владельца или текущий Executor сделки. Иначе — CRM_DEAL_FORBIDDEN (403).
  4. Система проверяет вход и предусловия: формат идентификаторов и длину причины; сделка существует и принадлежит агентству-владельцу актора; сделка нетерминальна (INV-DEAL-9); кандидат NewExecutorUserId — активный агент этого агентства-владельца (guard членства); кандидат != текущему исполнителю (иначе no-op).
  5. Система атомарно перестраивает состав (INV-DEAL-3):
    • прежний Executor: при DemotePriorToCoAgent = true — роль меняется Executor → CoAgent (членство и доступ сохраняются); иначе участие снимается (доступ убирается, история переписки команде сохраняется);
    • новый Executor: если кандидат уже активный участник (напр. CoAgent/Responsible) — роль меняется на Executor; иначе добавляется новым активным участником ролью Executor;
    • инвариант «ровно один активный Executor» соблюдается в той же операции — сделка ни на миг не остаётся без исполнителя.
  6. Система обновляет денормализованный ExecutorAgentUserId = NewExecutorUserId (получатель комиссии), UpdatedByUserId/UpdatedAt, инкрементирует версию; в таймлайн добавляет пару MemberRemoved(прежний) + MemberAdded(новый) с ролью Executor — фиксируя смену будущего получателя комиссии. Origin-поля (OriginAgentUserId/OriginAgencyCompanyId) не пересчитываются (INV-DEAL-10); ResponsibleAgentUserId и PrimaryInvestorUserId не трогаются — всё в одном согласованном изменении.
  7. Система фиксирует факт: публикует deal-members-changed.md (pin.crm.deal-members-changed.v1, key DealId) с дельтой ChangeType = ExecutorChanged и полным активным составом.
  8. Система возвращает карточку сделки с новым ExecutorAgentUserId, полным составом и свежей версией (HTTP 200); интерфейс обновляет исполнителя без повторного запроса.

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

A1. Понижение прежнего исполнителя до со-агента

  1. Актор задаёт DemotePriorToCoAgent = true.
  2. Прежний Executor остаётся в составе ролью CoAgent (членство и доступ сохранены); новый — единственный активный Executor.
Возврат к основному потоку

Продолжение — шаг 6.

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

  1. NewExecutorUserId — агент другого агентства либо без активного членства.
  2. Система отклоняет запрос CRM_PARTICIPANT_NOT_IN_AGENCY (422): исполнителем можно назначить только активного агента агентства-владельца сделки. Состав не меняется.
Завершается ошибкой

422 CRM_PARTICIPANT_NOT_IN_AGENCY — состав не меняется.

A3. Повтор с текущим исполнителем (no-op)

  1. NewExecutorUserId == ExecutorAgentUserId.
  2. Система возвращает текущую карточку (200) без мутаций и событий.
Завершается без изменений

No-op: состав, денормализация и события не меняются.

A4. Терминальная сделка — гейт получателя комиссии

  1. Сделка уже Completed (или Cancelled).
  2. Система отклоняет запрос CRM_DEAL_ALREADY_TERMINAL (409): получатель комиссии зафиксирован snapshot-ом на момент завершения (CommissionRecipientUserId = Executor), а после подтверждения начисления застройщиком в rewards (Confirmed) получатель закрепляется — сменить его переназначением исполнителя нельзя; ошибочный получатель лечится только компенсирующей корректировкой в rewards.
Завершается ошибкой

409 CRM_DEAL_ALREADY_TERMINAL — получатель комиссии зафиксирован, переназначение исполнителя его не меняет.

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

  1. Параллельная передача ведения/правка состава либо устаревший RowVersion.
  2. Оптимистическая блокировка: побеждает первый коммит; проигравший получает CRM_DEAL_CONFLICT (409).
Завершается ошибкой

409 CRM_DEAL_CONFLICT — клиент перечитывает карточку и повторяет при необходимости.

Постусловие

  • Активный Executor сделки — NewExecutorUserId (ровно один); прежний снят из состава (или понижен до CoAgent при DemotePriorToCoAgent); денормализованный ExecutorAgentUserId обновлён; версия инкрементирована; в таймлайн добавлены MemberRemoved+MemberAdded (роль Executor).
  • Origin-атрибуция (OriginAgentUserId/OriginAgencyCompanyId) не изменена (INV-DEAL-10).
  • Опубликован DealMembersChangedEvent (ChangeType = ExecutorChanged, полный активный состав).
  • Окно eventual-consistency: актор видит нового исполнителя сразу; синхронизация участников чата (messenger снимает доступ прежнего, приглашает нового) и уведомление новому исполнителю (notifications) — с лагом консюмеров (< 1 мин); порядок изменений per-сделка гарантирован ключом DealId.
  • No-op (кандидат == текущий): состав, денормализация и события не изменены.
  • При отказе — ни состав, ни денормализация, ни события.

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

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400формат DealId/NewExecutorUserId или длина ReasonПодсветить поля формы
CRM_DEAL_NOT_FOUND404сделки нет или она чужого агентстваОбновить список сделок
CRM_DEAL_FORBIDDEN403актор — не руководитель/админ агентства-владельца и не текущий ExecutorСкрыть действие «передать ведение»
CRM_PARTICIPANT_NOT_IN_AGENCY422кандидат — не активный агент агентства-владельцаПоказать «кандидат не состоит в агентстве»
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9) — получатель комиссии зафиксированПоказать «сделка завершена и не редактируется»
CRM_DEAL_CONFLICT409конкурентная правка состава / устаревший RowVersionПеречитать карточку и повторить

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

СобытиеКогда поднимаетсяSubscribers
DealExecutorReassignedАтомарная смена исполнителяподписчик таймлайна (MemberRemoved+MemberAdded, роль Executor), сборщик DealMembersChangedEvent

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

СобытиеTopicКогда публикуетсяConsumers
DealMembersChangedEventpin.crm.deal-members-changed.v1Передача ведения — контрактmessenger (снять доступ прежнего, пригласить нового в чат), notifications (уведомить нового исполнителя)

Гейт получателя комиссии обеспечивается downstream: при завершении сделки — deal-completed.md (CommissionRecipientUserId = ExecutorAgentUserId на момент завершения) → rewards фиксирует получателя в подтверждённом распределении (commission-accrual.md); после Confirmed смена получателя переназначением исполнителя недоступна.

Acceptance Criteria

#КритерийПроверка
AC1Передача ведения агенту B (снятие прежнего A) в нетерминальной сделке → 200; активный Executor — B, участие A снято, ExecutorAgentUserId = B, таймлайн MemberRemoved(A)+MemberAdded(B), событие deal-members-changed (ExecutorChanged, полный состав)Тест операции + проверка таймлайна/outbox
AC2DemotePriorToCoAgent = true → прежний Executor остаётся CoAgent, новый — единственный ExecutorТест понижения
AC3Атомарность каркаса: в любой момент операции — ровно один активный Executor (сделка не остаётся без исполнителя)Инвариант INV-DEAL-3
AC4Кандидат не из агентства-владельца / без активного членства → 422 CRM_PARTICIPANT_NOT_IN_AGENCY, состав не изменёнGuard членства
AC5NewExecutorUserId == ExecutorAgentUserId200 с текущей карточкой, без мутаций и событийNo-op тест
AC6Передача на терминальной сделке → 409 CRM_DEAL_ALREADY_TERMINAL (получатель комиссии зафиксирован)Гейт получателя
AC7Актор — рядовой со-агент (не руководитель и не текущий Executor) → 403 CRM_DEAL_FORBIDDENRecord-level тест
AC8OriginAgentUserId/OriginAgencyCompanyId не изменены после передачи веденияИммутабельность INV-DEAL-10
AC9Гонка / устаревший RowVersion409 CRM_DEAL_CONFLICT; проигравший перечитывает карточкуОптимистическая блокировка
AC10Через ≤ 1 мин после успеха messenger снимает доступ прежнего и приглашает нового, notifications уведомляет нового исполнителяDownstream (eventual)

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

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

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