UC-CRM-009. Передача ведения сделки (смена исполнителя)
Назначение
Описывает сценарий, в котором руководитель/администратор агентства-владельца (или сам текущий исполнитель —
передавая ведение другому агенту) назначает по сделке нового исполнителя (Executor) из активных агентов
своего агентства. Исполнитель — единственный получатель комиссии, поэтому передача ведения одновременно
переносит будущего получателя вознаграждения. Смена происходит атомарно: инвариант «ровно один активный
Executor» соблюдается в той же операции — сделка ни на миг не остаётся без исполнителя. Историческая
Origin-атрибуция («кто привёл клиента») иммутабельна (INV-DEAL-10) и не пересчитывается. После фиксации
получателя комиссии (завершение сделки + подтверждение начисления в rewards) сменить получателя передачей
ведения нельзя. Агрегат — DealEntity (INV-DEAL-3, INV-DEAL-10). Связан с контекстом crm.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-009 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной (+ альтернативные потоки A1–A5) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Руководитель/администратор агентства-владельца или текущий активный Executor сделки (передача «от себя» другому агенту) |
| Permissions / scope | crm.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) |
Основной Поток
- Актор открывает карточку сделки и выбирает «Передать ведение»: указывает нового исполнителя
(
NewExecutorUserId), опционально — оставить ли прежнего исполнителя в составе со-агентом (DemotePriorToCoAgent), причину (Reason) и наблюдаемую версию карточки для оптимистической блокировки. - Интерфейс отправляет запрос смены исполнителя в маршруте сделки.
- Система определяет доступ:
crm.deals.manage; актор — руководитель/админ агентства-владельца или текущийExecutorсделки. Иначе —CRM_DEAL_FORBIDDEN(403). - Система проверяет вход и предусловия: формат идентификаторов и длину причины; сделка существует и
принадлежит агентству-владельцу актора; сделка нетерминальна (INV-DEAL-9); кандидат
NewExecutorUserId— активный агент этого агентства-владельца (guard членства); кандидат!=текущему исполнителю (иначе no-op). - Система атомарно перестраивает состав (INV-DEAL-3):
- прежний
Executor: приDemotePriorToCoAgent = true— роль меняетсяExecutor → CoAgent(членство и доступ сохраняются); иначе участие снимается (доступ убирается, история переписки команде сохраняется); - новый
Executor: если кандидат уже активный участник (напр.CoAgent/Responsible) — роль меняется наExecutor; иначе добавляется новым активным участником рольюExecutor; - инвариант «ровно один активный
Executor» соблюдается в той же операции — сделка ни на миг не остаётся без исполнителя.
- прежний
- Система обновляет денормализованный
ExecutorAgentUserId = NewExecutorUserId(получатель комиссии),UpdatedByUserId/UpdatedAt, инкрементирует версию; в таймлайн добавляет паруMemberRemoved(прежний) +MemberAdded(новый) с рольюExecutor— фиксируя смену будущего получателя комиссии. Origin-поля (OriginAgentUserId/OriginAgencyCompanyId) не пересчитываются (INV-DEAL-10);ResponsibleAgentUserIdиPrimaryInvestorUserIdне трогаются — всё в одном согласованном изменении. - Система фиксирует факт: публикует deal-members-changed.md
(
pin.crm.deal-members-changed.v1, keyDealId) с дельтойChangeType = ExecutorChangedи полным активным составом. - Система возвращает карточку сделки с новым
ExecutorAgentUserId, полным составом и свежей версией (HTTP200); интерфейс обновляет исполнителя без повторного запроса.
Альтернативные Потоки
A1. Понижение прежнего исполнителя до со-агента
- Актор задаёт
DemotePriorToCoAgent = true. - Прежний
Executorостаётся в составе рольюCoAgent(членство и доступ сохранены); новый — единственный активныйExecutor.
Продолжение — шаг 6.
A2. Кандидат не из агентства-владельца
NewExecutorUserId— агент другого агентства либо без активного членства.- Система отклоняет запрос
CRM_PARTICIPANT_NOT_IN_AGENCY(422): исполнителем можно назначить только активного агента агентства-владельца сделки. Состав не меняется.
422 CRM_PARTICIPANT_NOT_IN_AGENCY — состав не меняется.
A3. Повтор с текущим исполнителем (no-op)
NewExecutorUserId == ExecutorAgentUserId.- Система возвращает текущую карточку (
200) без мутаций и событий.
No-op: состав, денормализация и события не меняются.
A4. Терминальная сделка — гейт получателя комиссии
- Сделка уже
Completed(илиCancelled). - Система отклоняет запрос
CRM_DEAL_ALREADY_TERMINAL(409): получатель комиссии зафиксирован snapshot-ом на момент завершения (CommissionRecipientUserId = Executor), а после подтверждения начисления застройщиком вrewards(Confirmed) получатель закрепляется — сменить его переназначением исполнителя нельзя; ошибочный получатель лечится только компенсирующей корректировкой вrewards.
409 CRM_DEAL_ALREADY_TERMINAL — получатель комиссии зафиксирован, переназначение исполнителя его не меняет.
A5. Конкурентная правка состава (гонка)
- Параллельная передача ведения/правка состава либо устаревший
RowVersion. - Оптимистическая блокировка: побеждает первый коммит; проигравший получает
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_FAILED | 400 | формат DealId/NewExecutorUserId или длина Reason | Подсветить поля формы |
CRM_DEAL_NOT_FOUND | 404 | сделки нет или она чужого агентства | Обновить список сделок |
CRM_DEAL_FORBIDDEN | 403 | актор — не руководитель/админ агентства-владельца и не текущий Executor | Скрыть действие «передать ведение» |
CRM_PARTICIPANT_NOT_IN_AGENCY | 422 | кандидат — не активный агент агентства-владельца | Показать «кандидат не состоит в агентстве» |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) — получатель комиссии зафиксирован | Показать «сделка завершена и не редактируется» |
CRM_DEAL_CONFLICT | 409 | конкурентная правка состава / устаревший RowVersion | Перечитать карточку и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DealExecutorReassigned | Атомарная смена исполнителя | подписчик таймлайна (MemberRemoved+MemberAdded, роль Executor), сборщик DealMembersChangedEvent |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DealMembersChangedEvent | pin.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 |
| AC2 | DemotePriorToCoAgent = true → прежний Executor остаётся CoAgent, новый — единственный Executor | Тест понижения |
| AC3 | Атомарность каркаса: в любой момент операции — ровно один активный Executor (сделка не остаётся без исполнителя) | Инвариант INV-DEAL-3 |
| AC4 | Кандидат не из агентства-владельца / без активного членства → 422 CRM_PARTICIPANT_NOT_IN_AGENCY, состав не изменён | Guard членства |
| AC5 | NewExecutorUserId == ExecutorAgentUserId → 200 с текущей карточкой, без мутаций и событий | No-op тест |
| AC6 | Передача на терминальной сделке → 409 CRM_DEAL_ALREADY_TERMINAL (получатель комиссии зафиксирован) | Гейт получателя |
| AC7 | Актор — рядовой со-агент (не руководитель и не текущий Executor) → 403 CRM_DEAL_FORBIDDEN | Record-level тест |
| AC8 | OriginAgentUserId/OriginAgencyCompanyId не изменены после передачи ведения | Иммутабельность INV-DEAL-10 |
| AC9 | Гонка / устаревший RowVersion → 409 CRM_DEAL_CONFLICT; проигравший перечитывает карточку | Оптимистическая блокировка |
| AC10 | Через ≤ 1 мин после успеха messenger снимает доступ прежнего и приглашает нового, notifications уведомляет нового исполнителя | Downstream (eventual) |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-007. Добавление участника в сделку, UC-CRM-008. Снятие участника со сделки
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: reassign-executor.md (единый для сделки и лида); соседние операции состава — add-deal-participant.md, remove-deal-participant.md, reassign-responsible.md (смена куратора).
- Domain: deal.md (INV-DEAL-3, INV-DEAL-10,
ExecutorAgentUserId,ParticipantRole), deal-timeline-event.md (MemberAdded/MemberRemoved); членство агента — agent-membership.md. - События: deal-members-changed.md (
pin.crm.deal-members-changed.v1), deal-completed.md (CommissionRecipientUserId); начисление комиссии — commission-accrual.md. - Смежные UC: UC-CRM-007 (добавление участника), UC-CRM-008 (снятие участника), UC-CRM-005 (завершение сделки и получатель комиссии).
- ADR: 0025 scoped-rbac, 0052 tenancy, 0071 участники и владелец сделки.