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

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 areaagent
Feature groupdeals / leads (передача ведения)
Статусdraft
Документdocs/06-services/crm/api/agent/reassign-executor.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — руководитель/администратор агентства-владельца или текущий активный 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-фильтру
Auditupdate (UpdatedByUserId = actor, Version++); сделка — строки таймлайна MemberRemoved(прежний Executor) + MemberAdded(новый Executor), фиксирующие смену получателя комиссии (deal-timeline-event.md)
Доступ

Передать ведение может руководитель/администратор агентства-владельца (любой процесс своего агентства) или сам текущий Executor (только «от себя» — другому агенту того же агентства). Рядовой со-агент/ассистент ведение не передаёт.

HTTP-контракт

ПолеЗначение
MethodPUT
Routeсделка — /api/agent/crm/deals/{dealId}/executor; лид — /api/agent/crm/leads/{leadId}/executor
Success status200 (обновлённая карточка с новым составом)
Idempotency headerне применяется — повтор с тем же NewExecutorUserId = no-op (200, состав не меняется); гонка ловится проверкой версии
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealId / LeadIdrouteGuidДа!= Guid.Empty; существующий процесс агентства-владельца
NewExecutorUserIdbodyGuidДа!= Guid.Empty; активный агент (AgentMembership.Status = Active) агентства-владельца процесса
DemotePriorToCoAgentbodybool?Нет (default false)true — прежнего Executor оставить в составе ролью CoAgent (не снимать членство); false — снять участие прежнего исполнителя (доступ убирается, история сохраняется)
Reasonbodystring?Нет≤ 1000 после trim, если заданfree-text: в логи не писать целиком
RowVersionbodyuint?Нетнаблюдаемая версия карточки для оптимистической блокировки
ActorUserIdidentity claimsGuidДаиз JWT
ActorAgencyCompanyIdidentity claimsGuidДаагентство актора — предикат агентства-владельца процессаTenant/record isolation
IsAgencyHeadidentity claimsboolДаруководитель/админ агентства-владельца (доступ на любой процесс своего агентства)

Модель ответа ProcessMembershipModel

Проекция процесса после передачи ведения (дом полей — deal.md / lead.md; здесь только дельта операции). Возвращается активный состав для немедленного обновления UI без повторного запроса.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealEntity.Id / LeadEntity.IdИдентификатор процесса.
Kindstring (Deal / Lead)ДароутТип процесса.
ExecutorAgentUserIdGuidДаExecutorAgentUserId (после смены)Новый текущий исполнитель (получатель комиссии).
ResponsibleAgentUserIdGuid?НетResponsibleAgentUserIdОтветственный/куратор (передачей исполнителя не меняется).
OriginAgentUserIdGuidДаOriginAgentUserIdИммутабельная Origin-атрибуция (не меняется).
OriginAgencyCompanyIdGuidДаOriginAgencyCompanyId (сделка) / TenantId (лид)Иммутабельное Origin-агентство.
ActiveMembersList<{ UserId: Guid, Role: string (enum ParticipantRole) }>Даактивные DealMemberEntity / LeadParticipantEntity (RemovedAt is null)Полный активный состав после передачи (ровно один Executor).
UpdatedAtDateTimeOffsetДаUpdatedAtМомент передачи.
RowVersionuintДаVersionТокен для следующей операции.

Алгоритм

Гейт получателя комиссии

Передача исполнителя доступна только на нетерминальном процессе. После того как получатель комиссии подтверждён в rewards (Confirmed), сменить его переназначением Executor уже нельзя — только компенсирующей корректировкой в rewards. Подробности — в шаге 6.

  1. Контекст и доступ. Операцию выполняет руководитель/админ агентства-владельца (permission crm.deals.manage / crm.leads.manage) либо сам текущий Executor процесса.
  2. Проверка входа и предусловий.
    • формат: 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 (возврат текущей карточки, мутаций и событий нет).
  3. Что читаем. Процесс (родитель мутации) и его активный состав участников; членство кандидата — по реплике/ справке identity agent-membership агентства-владельца.
  4. Бизнес-правила и переходы (передача ведения, 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-3 CRM_LAST_EXECUTOR_REQUIRED / INV-LEAD-8 CRM_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.
  5. Что меняется. Состав участников (снятие/смена роли прежнего + назначение нового), денормализованный ExecutorAgentUserId, для сделки — строки таймлайна MemberRemoved+MemberAdded (роль Executor) — в одном согласованном изменении.
  6. Гейт получателя комиссии (после подтверждения начисления). Передача исполнителя доступна только на нетерминальном процессе; на завершённой сделке (Completed) она заблокирована CRM_DEAL_ALREADY_TERMINAL. Получатель комиссии фиксируется snapshot-ом на момент завершения сделки (CommissionRecipientUserId = ExecutorAgentUserId, deal-completed.md); далее в rewards начисление подтверждается застройщиком и получатель (AgentUserId) закрепляется в подтверждённом распределении (Confirmed, commission-accrual.md, INV-3/INV-11) — сменить получателя переназначением исполнителя после этого нельзя: ошибочный получатель лечится только компенсирующей корректировкой в rewards, а не разворотом атрибуции в CRM.
  7. Результат. Карточка процесса с новым ExecutorAgentUserId, полным ActiveMembers, UpdatedAt, свежим RowVersion → HTTP 200. Параллельная передача/правка состава разрешается оптимистической блокировкой (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.

Eventual consistency

Актор видит нового исполнителя сразу; синхронизация участников чата (messenger) и уведомление новому исполнителю (notifications) приходят с лагом (SLA консюмеров < 1 мин); порядок изменений per-process гарантирован ключом DealId/LeadId.

Постусловие (no-op, NewExecutorUserId == ExecutorAgentUserId). Состав, денормализация и outbox не изменены; возвращается текущая карточка.

Постусловие (отказ). Ни состав участников, ни денормализация, ни события.

Побочные эффекты

ТипДетали
Интеграционные событиясделка — DealMembersChangedEventpin.crm.deal-members-changed.v1 (ChangeType = ExecutorChanged); лид — LeadParticipantsChangedEventpin.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-changedmessenger реконсилирует участников чата (доступ прежнего снят, новый приглашён), notifications уведомляет нового исполнителя о передаче ведения

Ошибки и failure modes

Ошибки

Терминальный процесс необратим: на завершённой/отменённой сделке или неактивном лиде передача ведения заблокирована 409 — после подтверждения начисления в rewards получателя комиссии так уже не сменить. Гонка с параллельной правкой состава / устаревшим RowVersion даёт 409 CONFLICT (retry после перечитки).

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат DealId/LeadId/NewExecutorUserId/Reason«Проверьте корректность запроса»no retry
CRM_DEAL_NOT_FOUND404сделки нет или она чужого агентства«Сделка не найдена»no retry
CRM_LEAD_NOT_FOUND404лида нет или он чужого агентства«Лид не найден»no retry
CRM_DEAL_FORBIDDEN403актор — не руководитель/админ агентства-владельца и не текущий Executor сделки«Нет доступа к передаче ведения»no retry
CRM_LEAD_FORBIDDEN403то же для лида«Нет доступа к передаче ведения»no retry
CRM_PARTICIPANT_NOT_IN_AGENCY422NewExecutorUserId — не активный агент агентства-владельца процесса«Кандидат не состоит в агентстве»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9) — получатель комиссии уже зафиксирован«Сделка завершена и не редактируется»no retry
CRM_LEAD_NOT_ACTIVE409лид Expired/Archived (INV-LEAD-2)«Лид неактивен»no retry
CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT409конкурентная правка состава/устаревший 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/ФИО не пишутся); metrics crm_executor_reassigned_total{kind}, latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path (сделка, снятие прежнего). Given нетерминальная сделка с Executor A и агент B — активный агент агентства-владельца; When руководитель передаёт ведение B без DemotePriorToCoAgent; Then активный Executor — B, участие A снято, ExecutorAgentUserId = B, в таймлайне MemberRemoved(A)+MemberAdded(B), опубликован DealMembersChangedEvent (ExecutorChanged) с полным составом, 200.
  • Happy path (лид, передача от себя). Given лид Active, Executor A; When A передаёт ведение агенту B своего агентства; Then Executor — B, доступ A снят вместе с членством, OriginAgentUserId не изменён, опубликован LeadParticipantsChangedEvent (атомарная пара Removed(A)+ExecutorChanged(B)).
  • Понижение прежнего до со-агента. Given DemotePriorToCoAgent = true; When передача ведения; Then прежний Executor остаётся в составе ролью CoAgent (членство/доступ сохранены), новый — единственный Executor.
  • Кандидат не из агентства. Given NewExecutorUserId — агент другого агентства (или без активного членства); When передача; Then 422 CRM_PARTICIPANT_NOT_IN_AGENCY, состав не изменён.
  • No-op. Given NewExecutorUserId == ExecutorAgentUserId; When передача; Then 200 с текущей карточкой, без мутаций и событий.
  • Терминальная сделка (гейт получателя). Given сделка Completed; When передача ведения; Then 409 CRM_DEAL_ALREADY_TERMINAL — получатель комиссии уже зафиксирован (после подтверждения начисления в rewards смена получателя недоступна).
  • Неактивный лид. Given лид Expired/Archived; When передача; Then 409 CRM_LEAD_NOT_ACTIVE.
  • Чужой актор. Given актор — рядовой со-агент (не руководитель и не текущий Executor); When передача; Then 403 CRM_DEAL_FORBIDDEN / CRM_LEAD_FORBIDDEN.
  • Гонка. Given параллельная правка состава / устаревший RowVersion; When передача; Then 409 CONFLICT; проигравший перечитывает карточку.
  • Иммутабельность Origin. Given передача ведения; When состав меняется; Then OriginAgentUserId/ OriginAgencyCompanyId (лид — TenantId) не изменены (INV-DEAL-10 / INV-LEAD-6).
  • Downstream (eventual). Given успешная передача; When проходит ≤ 1 мин; Then messenger снимает доступ прежнего и приглашает нового в чат, notifications уведомляет нового исполнителя.

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

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

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