PUT /api/agent/crm/{deals|leads}/{id}/executor — Передать ведение (сменить исполнителя)
Руководитель/администратор агентства (или сам текущий исполнитель — для передачи «на себя не работает»,
только другому агенту) назначает нового исполнителя (Executor) процесса CRM — лида или сделки — из
активных агентов агентства-владельца. Исполнитель — единственный получатель комиссии, поэтому передача
ведения одновременно переносит будущего получателя вознаграждения. Операция едина для лида
(lead.md, INV-LEAD-7/8) и сделки (deal.md, INV-DEAL-3):
меняется только носитель роли Executor, историческая Origin-атрибуция иммутабельна.
Назначение
Дать агентской стороне переназначить ведение процесса без потери исполнителя: агент уходит в отпуск/увольняется/
перегружен — руководитель передаёт лид или сделку другому агенту своего агентства. Новый Executor становится
единственным активным исполнителем и будущим получателем комиссии; прежний по умолчанию теряет членство и доступ
(история переписки в чате сохраняется команде). Origin-снимок (кто привёл клиента) не меняется — историческая
воронка и основание начислений сохраняются.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | deals / leads (передача ведения) |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/reassign-executor.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — руководитель/администратор агентства-владельца или текущий активный Executor процесса (передача «от себя») |
| Auth policy / Permissions | сделка — crm.deals.manage; лид — crm.leads.manage |
| Scope (RBAC) | организация (агентство-владелец): scope-иерархия ADR-0025 |
| Record-level | доступ по членству (ADR-0071): руководитель/админ агентства-владельца (для сделки — агентство активного Executor = OriginAgencyCompanyId в v1; для лида — TenantId = AgencyCompanyId) или сам текущий Executor; рядовой со-агент/ассистент ведение не передаёт |
| Tenant isolation | лид — TenantId = AgencyCompanyId; сделка — TenantId = DeveloperCompanyId (ADR-0052), агентский доступ — по предикату агентства-владельца + членству, а не по авто-tenant-фильтру |
| Audit | update (UpdatedByUserId = actor, Version++); сделка — строки таймлайна MemberRemoved(прежний Executor) + MemberAdded(новый Executor), фиксирующие смену получателя комиссии (deal-timeline-event.md) |
Передать ведение может руководитель/администратор агентства-владельца (любой процесс своего агентства) или
сам текущий Executor (только «от себя» — другому агенту того же агентства). Рядовой со-агент/ассистент
ведение не передаёт.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | сделка — /api/agent/crm/deals/{dealId}/executor; лид — /api/agent/crm/leads/{leadId}/executor |
| Success status | 200 (обновлённая карточка с новым составом) |
| Idempotency header | не применяется — повтор с тем же NewExecutorUserId = no-op (200, состав не меняется); гонка ловится проверкой версии |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId / LeadId | route | Guid | Да | != Guid.Empty; существующий процесс агентства-владельца | — |
NewExecutorUserId | body | Guid | Да | != Guid.Empty; активный агент (AgentMembership.Status = Active) агентства-владельца процесса | — |
DemotePriorToCoAgent | body | bool? | Нет (default false) | true — прежнего Executor оставить в составе ролью CoAgent (не снимать членство); false — снять участие прежнего исполнителя (доступ убирается, история сохраняется) | — |
Reason | body | string? | Нет | ≤ 1000 после trim, если задан | free-text: в логи не писать целиком |
RowVersion | body | uint? | Нет | наблюдаемая версия карточки для оптимистической блокировки | — |
ActorUserId | identity claims | Guid | Да | из JWT | — |
ActorAgencyCompanyId | identity claims | Guid | Да | агентство актора — предикат агентства-владельца процесса | Tenant/record isolation |
IsAgencyHead | identity claims | bool | Да | руководитель/админ агентства-владельца (доступ на любой процесс своего агентства) | — |
Модель ответа ProcessMembershipModel
Проекция процесса после передачи ведения (дом полей — deal.md / lead.md; здесь только дельта операции). Возвращается активный состав для немедленного обновления UI без повторного запроса.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id / LeadEntity.Id | — | Идентификатор процесса. |
Kind | string (Deal / Lead) | Да | роут | — | Тип процесса. |
ExecutorAgentUserId | Guid | Да | ExecutorAgentUserId (после смены) | — | Новый текущий исполнитель (получатель комиссии). |
ResponsibleAgentUserId | Guid? | Нет | ResponsibleAgentUserId | — | Ответственный/куратор (передачей исполнителя не меняется). |
OriginAgentUserId | Guid | Да | OriginAgentUserId | — | Иммутабельная Origin-атрибуция (не меняется). |
OriginAgencyCompanyId | Guid | Да | OriginAgencyCompanyId (сделка) / TenantId (лид) | — | Иммутабельное Origin-агентство. |
ActiveMembers | List<{ UserId: Guid, Role: string (enum ParticipantRole) }> | Да | активные DealMemberEntity / LeadParticipantEntity (RemovedAt is null) | — | Полный активный состав после передачи (ровно один Executor). |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент передачи. |
RowVersion | uint | Да | Version | — | Токен для следующей операции. |
Алгоритм
Передача исполнителя доступна только на нетерминальном процессе. После того как получатель комиссии
подтверждён в rewards (Confirmed), сменить его переназначением Executor уже нельзя — только
компенсирующей корректировкой в rewards. Подробности — в шаге 6.
- Контекст и доступ. Операцию выполняет руководитель/админ агентства-владельца (permission
crm.deals.manage/crm.leads.manage) либо сам текущийExecutorпроцесса. - Проверка входа и предусловий.
- формат:
DealId/LeadId/NewExecutorUserId!= Guid.Empty,Reason ≤ 1000— иначе ошибка валидации; - процесс существует и принадлежит агентству-владельцу актора — иначе
CRM_DEAL_NOT_FOUND/CRM_LEAD_NOT_FOUND(404; существование чужих процессов не раскрывается); - право актора:
IsAgencyHeadпо агентству-владельцу илиActorUserId == ExecutorAgentUserId(передача «от себя») — иначеCRM_DEAL_FORBIDDEN/CRM_LEAD_FORBIDDEN(403); - допустимость состояния: сделка не терминальна (
Status ∉ {Completed, Cancelled}, INV-DEAL-9) — иначеCRM_DEAL_ALREADY_TERMINAL(409); лид активен (Status = Active, INV-LEAD-2) — иначеCRM_LEAD_NOT_ACTIVE(409); - определяем агентство-владелец: для сделки — агентство активного
Executor(record-level ADR-0025; в v1 совпадает сOriginAgencyCompanyId); для лида —TenantId = AgencyCompanyId; - guard членства:
NewExecutorUserId— активный агент этого агентства-владельца (AgentMembership.Status = Active, agent-membership.md) — иначеCRM_PARTICIPANT_NOT_IN_AGENCY(422); - без no-op:
NewExecutorUserId != ExecutorAgentUserId— иначе no-op (возврат текущей карточки, мутаций и событий нет).
- формат:
- Что читаем. Процесс (родитель мутации) и его активный состав участников; членство кандидата — по реплике/
справке
identityagent-membership агентства-владельца. - Бизнес-правила и переходы (передача ведения, INV-DEAL-3 / INV-LEAD-7/8). Атомарно:
- прежний
Executor: еслиDemotePriorToCoAgent = true— роль меняетсяExecutor → CoAgent(членство и доступ сохраняются); иначе участие снимается (RemovedAt = now,RemovedByUserId = actor) — доступ убирается, история команде сохраняется; - новый
Executor: еслиNewExecutorUserIdуже активный участник (напр.CoAgent/Responsible) — роль меняется наExecutor; иначе добавляется новый активный участник рольюExecutor; - инвариант «ровно один активный
Executor» соблюдается в той же операции — процесс ни на миг не остаётся без исполнителя (INV-DEAL-3CRM_LAST_EXECUTOR_REQUIRED/ INV-LEAD-8CRM_LEAD_EXECUTOR_REQUIREDнедостижимы для этой операции, т.к. преемник назначается атомарно); - денормализация:
ExecutorAgentUserId = NewExecutorUserId;ResponsibleAgentUserId/PrimaryInvestorUserIdне трогаются;UpdatedByUserId = actor,UpdatedAt = now,Version++; - Origin-поля иммутабельны (INV-DEAL-10 / INV-LEAD-6):
OriginAgentUserId/OriginAgencyCompanyId(лид —TenantId) не пересчитываются — попытка правки на уровне агрегатаCRM_DEAL_SNAPSHOT_IMMUTABLE/CRM_LEAD_SNAPSHOT_IMMUTABLE.
- прежний
- Что меняется. Состав участников (снятие/смена роли прежнего + назначение нового), денормализованный
ExecutorAgentUserId, для сделки — строки таймлайнаMemberRemoved+MemberAdded(рольExecutor) — в одном согласованном изменении. - Гейт получателя комиссии (после подтверждения начисления). Передача исполнителя доступна только на
нетерминальном процессе; на завершённой сделке (
Completed) она заблокированаCRM_DEAL_ALREADY_TERMINAL. Получатель комиссии фиксируется snapshot-ом на момент завершения сделки (CommissionRecipientUserId = ExecutorAgentUserId, deal-completed.md); далее вrewardsначисление подтверждается застройщиком и получатель (AgentUserId) закрепляется в подтверждённом распределении (Confirmed, commission-accrual.md, INV-3/INV-11) — сменить получателя переназначением исполнителя после этого нельзя: ошибочный получатель лечится только компенсирующей корректировкой вrewards, а не разворотом атрибуции в CRM. - Результат. Карточка процесса с новым
ExecutorAgentUserId, полнымActiveMembers,UpdatedAt, свежимRowVersion→ HTTP200. Параллельная передача/правка состава разрешается оптимистической блокировкой (Version): побеждает первый коммит, проигравший получаетCRM_DEAL_CONFLICT/CRM_LEAD_CONFLICTи перечитывает карточку.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — руководитель/админ агентства-владельца или текущий Executor с соответствующим
permission. Процесс нетерминален (сделка Status ∉ {Completed, Cancelled}; лид Status = Active).
NewExecutorUserId — активный агент агентства-владельца и != текущего исполнителя.
Постусловие (успех). Активный Executor процесса — NewExecutorUserId (ровно один); прежний исполнитель
снят из состава (или понижен до CoAgent при DemotePriorToCoAgent); денормализованный ExecutorAgentUserId
обновлён; UpdatedByUserId = actor, UpdatedAt = now, Version++. Origin-атрибуция (OriginAgentUserId,
OriginAgencyCompanyId/TenantId) не изменена. Для сделки в таймлайн добавлены MemberRemoved+MemberAdded
(роль Executor). Опубликовано DealMembersChangedEvent / LeadParticipantsChangedEvent с полным активным
составом и дельтой ChangeType = ExecutorChanged.
Актор видит нового исполнителя сразу; синхронизация участников чата (messenger) и уведомление новому
исполнителю (notifications) приходят с лагом (SLA консюмеров < 1 мин); порядок изменений per-process
гарантирован ключом DealId/LeadId.
Постусловие (no-op, NewExecutorUserId == ExecutorAgentUserId). Состав, денормализация и outbox не изменены;
возвращается текущая карточка.
Постусловие (отказ). Ни состав участников, ни денормализация, ни события.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | сделка — DealMembersChangedEvent → pin.crm.deal-members-changed.v1 (ChangeType = ExecutorChanged); лид — LeadParticipantsChangedEvent → pin.crm.lead-participants-changed.v1 (ChangeType = ExecutorChanged, атомарная пара Removed+ExecutorChanged); всё в outbox, key = DealId/LeadId |
| Внешние вызовы | проверка активного членства кандидата в агентстве-владельце — реплика/справка identity agent-membership (timeout 2 c) |
| Проекции / поиск | денормализованный ExecutorAgentUserId обновляется (индексы «мои процессы» исполнителя); проекция поиска сделок (DealSearchDocument) по исполнителю пересобирается; Title/ФИО не затронуты |
| Уведомления | опосредованно: консюмеры deal-members-changed/lead-participants-changed — messenger реконсилирует участников чата (доступ прежнего снят, новый приглашён), notifications уведомляет нового исполнителя о передаче ведения |
Ошибки и failure modes
Терминальный процесс необратим: на завершённой/отменённой сделке или неактивном лиде передача ведения
заблокирована 409 — после подтверждения начисления в rewards получателя комиссии так уже не сменить.
Гонка с параллельной правкой состава / устаревшим RowVersion даёт 409 CONFLICT (retry после перечитки).
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат DealId/LeadId/NewExecutorUserId/Reason | «Проверьте корректность запроса» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки нет или она чужого агентства | «Сделка не найдена» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лида нет или он чужого агентства | «Лид не найден» | no retry |
CRM_DEAL_FORBIDDEN | 403 | актор — не руководитель/админ агентства-владельца и не текущий Executor сделки | «Нет доступа к передаче ведения» | no retry |
CRM_LEAD_FORBIDDEN | 403 | то же для лида | «Нет доступа к передаче ведения» | no retry |
CRM_PARTICIPANT_NOT_IN_AGENCY | 422 | NewExecutorUserId — не активный агент агентства-владельца процесса | «Кандидат не состоит в агентстве» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) — получатель комиссии уже зафиксирован | «Сделка завершена и не редактируется» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | лид Expired/Archived (INV-LEAD-2) | «Лид неактивен» | no retry |
CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT | 409 | конкурентная правка состава/устаревший RowVersion | «Данные изменены, обновите карточку» | retry (перечитать) |
Совместимость и наблюдаемость
- Контракт: route +
NewExecutorUserId/DemotePriorToCoAgent/Reason/RowVersion(body) →ProcessMembershipModel. Новые optional-поля — additive; смена маршрута/семантики роли — breaking (migration + rollout). Новые значенияParticipantRole— additive (см. deal-members-changed.md). - Logs (без PII):
executor reassigned(DealId/LeadId,FromExecutorUserId → ToExecutorUserId,ActorUserId,DemotePriorToCoAgent;Reason/ФИО не пишутся); metricscrm_executor_reassigned_total{kind}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path (сделка, снятие прежнего). Given нетерминальная сделка с
ExecutorA и агент B — активный агент агентства-владельца; When руководитель передаёт ведение B безDemotePriorToCoAgent; Then активныйExecutor— B, участие A снято,ExecutorAgentUserId = B, в таймлайнеMemberRemoved(A)+MemberAdded(B), опубликованDealMembersChangedEvent(ExecutorChanged) с полным составом,200. - Happy path (лид, передача от себя). Given лид
Active,ExecutorA; When A передаёт ведение агенту B своего агентства; ThenExecutor— B, доступ A снят вместе с членством,OriginAgentUserIdне изменён, опубликованLeadParticipantsChangedEvent(атомарная параRemoved(A)+ExecutorChanged(B)). - Понижение прежнего до со-агента. Given
DemotePriorToCoAgent = true; When передача ведения; Then прежнийExecutorостаётся в составе рольюCoAgent(членство/доступ сохранены), новый — единственныйExecutor. - Кандидат не из агентства. Given
NewExecutorUserId— агент другого агентства (или без активного членства); When передача; Then422 CRM_PARTICIPANT_NOT_IN_AGENCY, состав не изменён. - No-op. Given
NewExecutorUserId == ExecutorAgentUserId; When передача; Then200с текущей карточкой, без мутаций и событий. - Терминальная сделка (гейт получателя). Given сделка
Completed; When передача ведения; Then409 CRM_DEAL_ALREADY_TERMINAL— получатель комиссии уже зафиксирован (после подтверждения начисления вrewardsсмена получателя недоступна). - Неактивный лид. Given лид
Expired/Archived; When передача; Then409 CRM_LEAD_NOT_ACTIVE. - Чужой актор. Given актор — рядовой со-агент (не руководитель и не текущий
Executor); When передача; Then403 CRM_DEAL_FORBIDDEN/CRM_LEAD_FORBIDDEN. - Гонка. Given параллельная правка состава / устаревший
RowVersion; When передача; Then409 CONFLICT; проигравший перечитывает карточку. - Иммутабельность Origin. Given передача ведения; When состав меняется; Then
OriginAgentUserId/OriginAgencyCompanyId(лид —TenantId) не изменены (INV-DEAL-10 / INV-LEAD-6). - Downstream (eventual). Given успешная передача; When проходит ≤ 1 мин; Then
messengerснимает доступ прежнего и приглашает нового в чат,notificationsуведомляет нового исполнителя.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): PUT /api/agent/crm/{deals|leads}/{id}/responsible — Назначить/снять ответственного (куратора)
- Use Cases (9): UC-CRM-007. Добавление участника в сделку, UC-CRM-008. Снятие участника со сделки, UC-CRM-009. Передача ведения сделки (смена исполнителя), UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель, UC-CRM-012. Управление составом участников лида (добавление/снятие: второй агент, супруг/юрист инвестора), UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии, UC-RWD-016. Застройщик подтверждает начисление после смены исполнителя, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности (дом полей, инварианты состава, передача ведения): deal.md (INV-DEAL-3/10),
lead.md (INV-LEAD-7/8, INV-LEAD-6); таймлайн:
deal-timeline-event.md (
MemberAdded/MemberRemoved). - События: deal-members-changed.md,
lead-participants-changed.md (
ChangeType = ExecutorChanged). - Гейт получателя комиссии: deal-completed.md (
CommissionRecipientUserId), commission-accrual.md (Confirmed, INV-3/INV-11). - Членство агента: agent-membership.md. Смежная операция — reassign-responsible.md. Карта областей — README контекста.
- ADR: 0025 scoped-rbac, 0052 tenancy, 0071 crm-ownership-and-participant-model.