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

UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии

Назначение

По сделке возникает спор о том, кто получит комиссию (например, ведение фактически перешло другому агенту, но исполнитель в системе остался прежним, либо два агента претендуют на один процесс). Руководитель агентства разрешает спор детерминированно, явно назначая корректного исполнителя (Executor) процесса: исполнитель — единственный источник истины исполнителя и получатель комиссии, поэтому передача ведения одновременно переносит будущего получателя вознаграждения. Origin-атрибуция («кто привёл клиента») при этом иммутабельна: она сохраняется для воронки агентства и антифрода, но не определяет получателя. Операция — передача ведения (reassign-executor.md); окно для смены получателя ограничено — после завершения сделки и подтверждения начисления получатель закреплён.

Metadata

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

Актор

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

Предусловие

  • Сделка существует, принадлежит агентству-владельцу актора и нетерминальна (Status ∉ {Completed, Cancelled}, INV-DEAL-9) — иначе получатель комиссии уже зафиксирован.
  • Кандидат NewExecutorUserIdактивный агент агентства-владельца (AgentMembership.Status = Active, agent-membership.md) и не совпадает с текущим Executor.
  • Начисление комиссии по сделке ещё не подтверждено в rewards (сделка не завершена) — иначе смена получателя переназначением недоступна (см. шаг 6).

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API
Триггерящий элемент«Передать ведение / назначить исполнителя» на карточке сделки /crmPUT /api/agent/crm/deals/{dealId}/executor

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

  1. Контекст и доступ. Руководитель агентства-владельца открывает спорную сделку и инициирует явное назначение исполнителя; проверяется crm.deals.manage и право по членству (руководитель/админ владельца или сам текущий Executor) — иначе CRM_DEAL_FORBIDDEN.
  2. Ввод. Форма: NewExecutorUserId (корректный получатель комиссии), опционально DemotePriorToCoAgent (оставить прежнего исполнителя в составе ролью CoAgent, не снимая доступ), Reason (комментарий по разрешению спора), RowVersion (наблюдаемая версия карточки).
  3. Проверка состояния и связей.
    • сделка существует и принадлежит агентству-владельцу актора — иначе CRM_DEAL_NOT_FOUND (404);
    • сделка нетерминальна — иначе CRM_DEAL_ALREADY_TERMINAL (409);
    • NewExecutorUserId — активный агент агентства-владельца — иначе CRM_PARTICIPANT_NOT_IN_AGENCY (422);
    • NewExecutorUserId != ExecutorAgentUserId — иначе no-op (текущая карточка без мутаций и событий).
  4. Передача ведения (атомарно, INV-DEAL-3).
    • прежний Executor: при DemotePriorToCoAgent = true — роль Executor → CoAgent (членство/доступ сохранены); иначе участие снимается (RemovedAt = now, RemovedByUserId = actor) — доступ убирается, история переписки команде сохраняется;
    • новый Executor: если уже активный участник (CoAgent/Responsible) — роль меняется на Executor; иначе добавляется активным участником ролью Executor;
    • инвариант «ровно один активный Executor» соблюдается в той же операции — сделка ни на миг не остаётся без исполнителя (CRM_LAST_EXECUTOR_REQUIRED для этой операции недостижим);
    • денормализация ExecutorAgentUserId = NewExecutorUserId; ResponsibleAgentUserId/PrimaryInvestorUserId не трогаются; Origin-поля иммутабельны (INV-DEAL-10) — OriginAgentUserId/OriginAgencyCompanyId не пересчитываются.
  5. Что меняется и публикация. Состав участников, ExecutorAgentUserId, строки таймлайна MemberRemoved (прежний) + MemberAdded (новый, роль Executor) — в одном согласованном изменении; публикуется DealMembersChangedEvent (ChangeType = ExecutorChanged) с полным активным составом.
  6. Гейт получателя комиссии. Смена получателя переназначением возможна только пока сделка нетерминальна. При завершении сделки (Sale → Completed) получатель фиксируется snapshot-ом CommissionRecipientUserId = ExecutorAgentUserId (deal-completed.md); далее в rewards начисление подтверждается застройщиком и получатель закрепляется в подтверждённом распределении (Confirmed, commission-accrual.md). После этого «переиграть» получателя переназначением исполнителя нельзя — ошибочный получатель лечится только компенсирующей корректировкой в rewards, а не разворотом атрибуции в CRM.
  7. Результат. Карточка сделки с новым ExecutorAgentUserId, полным активным составом (ровно один Executor), свежим RowVersion. При завершении сделки комиссия начислится назначенному исполнителю.

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

A1. Прежнего исполнителя оставляют со-агентом
  1. DemotePriorToCoAgent = true: прежний Executor остаётся в составе ролью CoAgent (доступ к сделке и чату сохранён, но комиссии он больше не получает — Executor один), новый — единственный Executor.

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

A2. Спорная сделка уже завершена (получатель зафиксирован)
  1. Шаг 3: сделка CompletedCRM_DEAL_ALREADY_TERMINAL (409). Получатель — CommissionRecipientUserId на момент завершения; переназначение недоступно.
  2. Спор разрешается компенсирующей корректировкой в rewards (commission-accrual.md), не в CRM.
A3. Кандидат не из агентства-владельца
  1. Шаг 3: NewExecutorUserId — агент другого агентства или без активного членства → CRM_PARTICIPANT_NOT_IN_AGENCY (422); состав не меняется. Исполнителем можно назначить только активного агента агентства-владельца.
A4. Актор не вправе передавать ведение
  1. Актор — рядовой со-агент/ассистент (не руководитель/админ владельца и не текущий Executor) → CRM_DEAL_FORBIDDEN (403); операция не выполняется.
A5. Конкурентная правка состава
  1. Параллельная передача ведения / правка состава с устаревшим RowVersionCRM_DEAL_CONFLICT (409); побеждает первый коммит, проигравший перечитывает карточку и повторяет.

Постусловие

  • Активный Executor сделки — NewExecutorUserId (ровно один); прежний снят из состава (или понижен до CoAgent при DemotePriorToCoAgent); ExecutorAgentUserId обновлён; UpdatedByUserId = actor, Version++.
  • Origin-атрибуция (OriginAgentUserId/OriginAgencyCompanyId) не изменена (INV-DEAL-10).
  • В таймлайне — MemberRemoved (прежний) + MemberAdded (новый, роль Executor); опубликован DealMembersChangedEvent (ChangeType = ExecutorChanged) с полным активным составом.
  • При будущем завершении сделки комиссия начислится назначенному исполнителю (CommissionRecipientUserId = ExecutorAgentUserId на момент завершения); основание/атрибуция — иммутабельный Origin.
  • (No-op / отказ) состав, денормализация и события не изменены.

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

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400Некорректный DealId/NewExecutorUserId/Reason«Проверьте корректность запроса»
CRM_DEAL_NOT_FOUND404Сделки нет или она чужого агентства«Сделка не найдена»
CRM_DEAL_FORBIDDEN403Актор — не руководитель/админ владельца и не текущий Executor (A4)«Нет доступа к передаче ведения»
CRM_PARTICIPANT_NOT_IN_AGENCY422NewExecutorUserId — не активный агент агентства-владельца (A3)«Кандидат не состоит в агентстве»
CRM_DEAL_ALREADY_TERMINAL409Сделка Completed/Cancelled — получатель зафиксирован (A2)«Сделка завершена; спор — через корректировку в rewards»
CRM_DEAL_CONFLICT409Конкурентная правка состава / устаревший RowVersion (A5)«Данные изменены, обновите карточку»

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

СобытиеКогда поднимаетсяSubscribers
DealMemberRemovedСнятие/понижение прежнего ExecutorТаймлайн сделки (MemberRemoved) — в том же согласованном изменении
DealMemberAddedНазначение нового ExecutorТаймлайн сделки (MemberAdded, роль Executor)

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

СобытиеTopicКогда публикуетсяConsumers
DealMembersChangedEvent.v1pin.crm.deal-members-changed.v1Передача ведения (ChangeType = ExecutorChanged), через outbox, key = DealIdmessenger (снять доступ прежнего, пригласить нового в чат, история сохранена), notifications (уведомить нового исполнителя) — контракт deal-members-changed.md

Получатель комиссии закрепляется позже — при завершении сделки: DealCompletedEvent (deal-completed.md) несёт CommissionRecipientUserId = ExecutorAgentUserId в rewards.

Acceptance Criteria

#КритерийПроверка
AC1Руководитель назначает нового Executor на нетерминальной сделке → ровно один активный Executor = кандидат, ExecutorAgentUserId обновлён, таймлайн MemberRemoved+MemberAdded, DealMembersChangedEvent (ExecutorChanged)Интеграционный тест PUT /deals/{id}/executor
AC2При завершении сделки комиссия начисляется назначенному исполнителю (CommissionRecipientUserId = Executor)Тест DealCompletedEventrewards получатель = новый Executor
AC3Origin-атрибуция (OriginAgentUserId/OriginAgencyCompanyId) не измененаТест: снапшоты неизменны (INV-DEAL-10)
AC4Завершённая сделка → CRM_DEAL_ALREADY_TERMINAL; смена получателя только корректировкой в rewards (A2)Негативный тест на Completed
AC5Кандидат не из агентства-владельца → CRM_PARTICIPANT_NOT_IN_AGENCY (A3)Негативный тест
AC6Рядовой со-агент → CRM_DEAL_FORBIDDEN (A4)Негативный тест доступа
AC7DemotePriorToCoAgent = true → прежний остаётся CoAgent, комиссии не получает (A1)Тест состава после передачи
AC8Гонка/устаревший RowVersionCRM_DEAL_CONFLICT, проигравший перечитывает (A5)Тест конкурентной передачи

Идемпотентность, конкурентность и восстановление

  • Идемпотентность. Повтор с тем же NewExecutorUserId — no-op (200, состав не меняется); отдельный idempotency-ключ не нужен. Consumers deal-members-changed идемпотентны по MessageId + Version-guard на DealId.
  • Конкурентность (A5). Параллельная передача/правка состава сериализуется оптимистической блокировкой (Version): первый коммит побеждает, проигравший получает CRM_DEAL_CONFLICT и перечитывает карточку.
  • Восстановление. При любом отказе (A2–A5) мутаций нет; состав, денормализация и таймлайн — в одном согласованном изменении, частичной записи быть не может.

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

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

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