UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии
Назначение
По сделке возникает спор о том, кто получит комиссию (например, ведение фактически перешло другому агенту, но
исполнитель в системе остался прежним, либо два агента претендуют на один процесс). Руководитель агентства
разрешает спор детерминированно, явно назначая корректного исполнителя (Executor) процесса: исполнитель —
единственный источник истины исполнителя и получатель комиссии, поэтому передача ведения одновременно
переносит будущего получателя вознаграждения. Origin-атрибуция («кто привёл клиента») при этом иммутабельна:
она сохраняется для воронки агентства и антифрода, но не определяет получателя. Операция — передача ведения
(reassign-executor.md); окно для смены получателя ограничено — после
завершения сделки и подтверждения начисления получатель закреплён.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-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 |
| Триггерящий элемент | «Передать ведение / назначить исполнителя» на карточке сделки /crm → PUT /api/agent/crm/deals/{dealId}/executor |
Основной Поток
- Контекст и доступ. Руководитель агентства-владельца открывает спорную сделку и инициирует явное
назначение исполнителя; проверяется
crm.deals.manageи право по членству (руководитель/админ владельца или сам текущийExecutor) — иначеCRM_DEAL_FORBIDDEN. - Ввод. Форма:
NewExecutorUserId(корректный получатель комиссии), опциональноDemotePriorToCoAgent(оставить прежнего исполнителя в составе рольюCoAgent, не снимая доступ),Reason(комментарий по разрешению спора),RowVersion(наблюдаемая версия карточки). - Проверка состояния и связей.
- сделка существует и принадлежит агентству-владельцу актора — иначе
CRM_DEAL_NOT_FOUND(404); - сделка нетерминальна — иначе
CRM_DEAL_ALREADY_TERMINAL(409); NewExecutorUserId— активный агент агентства-владельца — иначеCRM_PARTICIPANT_NOT_IN_AGENCY(422);NewExecutorUserId != ExecutorAgentUserId— иначе no-op (текущая карточка без мутаций и событий).
- сделка существует и принадлежит агентству-владельцу актора — иначе
- Передача ведения (атомарно, 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не пересчитываются.
- прежний
- Что меняется и публикация. Состав участников,
ExecutorAgentUserId, строки таймлайнаMemberRemoved(прежний) +MemberAdded(новый, рольExecutor) — в одном согласованном изменении; публикуетсяDealMembersChangedEvent(ChangeType = ExecutorChanged) с полным активным составом. - Гейт получателя комиссии. Смена получателя переназначением возможна только пока сделка нетерминальна.
При завершении сделки (
Sale → Completed) получатель фиксируется snapshot-омCommissionRecipientUserId = ExecutorAgentUserId(deal-completed.md); далее вrewardsначисление подтверждается застройщиком и получатель закрепляется в подтверждённом распределении (Confirmed, commission-accrual.md). После этого «переиграть» получателя переназначением исполнителя нельзя — ошибочный получатель лечится только компенсирующей корректировкой вrewards, а не разворотом атрибуции в CRM. - Результат. Карточка сделки с новым
ExecutorAgentUserId, полным активным составом (ровно одинExecutor), свежимRowVersion. При завершении сделки комиссия начислится назначенному исполнителю.
Альтернативные Потоки
DemotePriorToCoAgent = true: прежнийExecutorостаётся в составе рольюCoAgent(доступ к сделке и чату сохранён, но комиссии он больше не получает —Executorодин), новый — единственныйExecutor.
Сходится с основным потоком на шаге 5.
- Шаг 3: сделка
Completed→CRM_DEAL_ALREADY_TERMINAL(409). Получатель —CommissionRecipientUserIdна момент завершения; переназначение недоступно. - Спор разрешается компенсирующей корректировкой в
rewards(commission-accrual.md), не в CRM.
- Шаг 3:
NewExecutorUserId— агент другого агентства или без активного членства →CRM_PARTICIPANT_NOT_IN_AGENCY(422); состав не меняется. Исполнителем можно назначить только активного агента агентства-владельца.
- Актор — рядовой со-агент/ассистент (не руководитель/админ владельца и не текущий
Executor) →CRM_DEAL_FORBIDDEN(403); операция не выполняется.
- Параллельная передача ведения / правка состава с устаревшим
RowVersion→CRM_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_FAILED | 400 | Некорректный DealId/NewExecutorUserId/Reason | «Проверьте корректность запроса» |
CRM_DEAL_NOT_FOUND | 404 | Сделки нет или она чужого агентства | «Сделка не найдена» |
CRM_DEAL_FORBIDDEN | 403 | Актор — не руководитель/админ владельца и не текущий Executor (A4) | «Нет доступа к передаче ведения» |
CRM_PARTICIPANT_NOT_IN_AGENCY | 422 | NewExecutorUserId — не активный агент агентства-владельца (A3) | «Кандидат не состоит в агентстве» |
CRM_DEAL_ALREADY_TERMINAL | 409 | Сделка Completed/Cancelled — получатель зафиксирован (A2) | «Сделка завершена; спор — через корректировку в rewards» |
CRM_DEAL_CONFLICT | 409 | Конкурентная правка состава / устаревший RowVersion (A5) | «Данные изменены, обновите карточку» |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DealMemberRemoved | Снятие/понижение прежнего Executor | Таймлайн сделки (MemberRemoved) — в том же согласованном изменении |
DealMemberAdded | Назначение нового Executor | Таймлайн сделки (MemberAdded, роль Executor) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DealMembersChangedEvent.v1 | pin.crm.deal-members-changed.v1 | Передача ведения (ChangeType = ExecutorChanged), через outbox, key = DealId | messenger (снять доступ прежнего, пригласить нового в чат, история сохранена), 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) | Тест DealCompletedEvent → rewards получатель = новый Executor |
| AC3 | Origin-атрибуция (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) | Негативный тест доступа |
| AC7 | DemotePriorToCoAgent = true → прежний остаётся CoAgent, комиссии не получает (A1) | Тест состава после передачи |
| AC8 | Гонка/устаревший RowVersion → CRM_DEAL_CONFLICT, проигравший перечитывает (A5) | Тест конкурентной передачи |
Идемпотентность, конкурентность и восстановление
- Идемпотентность. Повтор с тем же
NewExecutorUserId— no-op (200, состав не меняется); отдельный idempotency-ключ не нужен. Consumersdeal-members-changedидемпотентны поMessageId+Version-guard наDealId. - Конкурентность (A5). Параллельная передача/правка состава сериализуется оптимистической блокировкой
(
Version): первый коммит побеждает, проигравший получаетCRM_DEAL_CONFLICTи перечитывает карточку. - Восстановление. При любом отказе (A2–A5) мутаций нет; состав, денормализация и таймлайн — в одном согласованном изменении, частичной записи быть не может.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Операция: reassign-executor.md; смежная — reassign-responsible.md.
- Domain: ../domain/deal.md (INV-DEAL-3 «ровно один активный
Executor», INV-DEAL-10 «иммутабельность Origin»,ExecutorAgentUserIdкак получатель комиссии), deal-timeline-event.md (MemberAdded/MemberRemoved), agent-membership.md (активное членство кандидата). - События: deal-members-changed.md (
ChangeType = ExecutorChanged), deal-completed.md (CommissionRecipientUserId— фиксация получателя). - Гейт/корректировка комиссии: commission-accrual.md (
Confirmed). - Смежные сценарии: UC-CRM-013 (обязательный reassign перед увольнением), UC-CRM-002 (начальный состав сделки).
- ADR: 0071 crm-ownership-and-participant-model, 0025 scoped-rbac, 0052 tenancy.