PUT /api/agent/crm/{deals|leads}/{id}/responsible — Назначить/снять ответственного (куратора)
Руководитель/администратор агентства-владельца (или текущий Executor) назначает или снимает ответственного
(Responsible) процесса CRM — лида или сделки. Ответственный — опциональный куратор ведения (руководитель
группы, наставник), может отличаться от исполнителя и не является получателем комиссии. Операция едина
для лида (lead.md, INV-LEAD-7) и сделки (deal.md, INV-DEAL-3):
меняется только носитель роли Responsible, исполнитель и Origin-атрибуция не затрагиваются.
Назначение
Дать агентству контроль над сложными процессами: закрепить за лидом/сделкой куратора, который видит процесс и
подсвечивается в чате, но не получает комиссию (её получает Executor). В отличие от передачи ведения
(reassign-executor.md) назначение/снятие ответственного не влияет на исполнителя и
получателя вознаграждения — это чисто организационная роль. Роль опциональна: активный Responsible — не более
одного; снятие возвращает процесс к состоянию «без куратора».
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | deals / leads (кураторство) |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/reassign-responsible.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), агентский доступ — по предикату агентства-владельца + членству |
| Audit | update (UpdatedByUserId = actor, Version++); сделка — строки таймлайна MemberAdded/MemberRemoved (роль Responsible, deal-timeline-event.md) |
Выполнить может руководитель/администратор агентства-владельца процесса (сделка — агентство активного
Executor = OriginAgencyCompanyId в v1; лид — TenantId = AgencyCompanyId) либо сам текущий Executor —
иначе CRM_DEAL_FORBIDDEN / CRM_LEAD_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | сделка — /api/agent/crm/deals/{dealId}/responsible; лид — /api/agent/crm/leads/{leadId}/responsible |
| Success status | 200 (обновлённая карточка с составом) |
| Idempotency header | не применяется — повтор с тем же ResponsibleUserId (или повторное снятие) = no-op (200); гонка ловится проверкой версии |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId / LeadId | route | Guid | Да | != Guid.Empty; существующий процесс агентства-владельца | — |
ResponsibleUserId | body | Guid? | Нет | null — снять ответственного; иначе != Guid.Empty, активный агент (AgentMembership.Status = Active) агентства-владельца | — |
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; здесь только дельта). Та же модель, что у reassign-executor.md.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id / LeadEntity.Id | — | Идентификатор процесса. |
Kind | string (Deal / Lead) | Да | роут | — | Тип процесса. |
ExecutorAgentUserId | Guid | Да | ExecutorAgentUserId | — | Текущий исполнитель (этой операцией не меняется). |
ResponsibleAgentUserId | Guid? | Нет | ResponsibleAgentUserId (после операции) | — | Новый ответственный/куратор; null — снят. |
ActiveMembers | List<{ UserId: Guid, Role: string (enum ParticipantRole) }> | Да | активные DealMemberEntity / LeadParticipantEntity (RemovedAt is null) | — | Полный активный состав (не более одного Responsible). |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент операции. |
RowVersion | uint | Да | Version | — | Токен для следующей операции. |
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/админ агентства-владельца (permission
crm.deals.manage/crm.leads.manage) либо текущийExecutorпроцесса. - Проверка входа и предусловий.
- формат:
DealId/LeadId!= Guid.Empty; при заданномResponsibleUserId—!= 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) — иначеCRM_LEAD_NOT_ACTIVE(409); - назначение (
ResponsibleUserId != null): кандидат — активный агент агентства-владельца (AgentMembership.Status = Active, agent-membership.md) — иначеCRM_PARTICIPANT_NOT_IN_AGENCY(422); - без no-op: назначение того же
ResponsibleUserIdпри уже совпадающем активномResponsible, либо снятие при отсутствующемResponsible— no-op (возврат текущей карточки, мутаций и событий нет).
- формат:
- Что читаем. Процесс (родитель мутации) и его активный состав; при назначении — членство кандидата по
реплике/справке
identityagent-membership. - Бизнес-правила и переходы (INV-DEAL-3 / INV-LEAD-7;
Responsibleопционален, ≤ 1 активный). Атомарно:- назначение: прежний активный
Responsible(если был) снимается (RemovedAt = now) либо, если это тот жеUserIdв другой роли — роль меняется; кандидат становится активнымResponsible(если уже участник иной нефинансовой роли — роль меняется наResponsible; иначе добавляется новым участником рольюResponsible).Responsibleможет совпадать или не совпадать сExecutor— ограничения «≠ Executor» нет; - снятие (
ResponsibleUserId = null): активныйResponsibleснимается из состава (RemovedAt = now); если тот же пользователь остаётся исполнителем (Executor) — снимается только роль куратора, участие исполнителя сохраняется; Executor/PrimaryInvestorUserIdне трогаются; денормализацияResponsibleAgentUserId = ResponsibleUserId(илиnull);UpdatedByUserId = actor,UpdatedAt = now,Version++;- Origin-поля иммутабельны (INV-DEAL-10 / INV-LEAD-6) — не пересчитываются.
- назначение: прежний активный
- Что меняется. Состав участников (назначение/снятие
Responsible), денормализованныйResponsibleAgentUserId, для сделки — строка таймлайнаMemberAdded/MemberRemoved(рольResponsible) — в одном согласованном изменении. Комиссия и получатель вознаграждения не затрагиваются (роль нефинансовая). - Результат. Карточка процесса с новым
ResponsibleAgentUserId(илиnull), полнымActiveMembers,UpdatedAt, свежимRowVersion→ HTTP200. Параллельная правка состава разрешается оптимистической блокировкой (Version): проигравший получаетCRM_DEAL_CONFLICT/CRM_LEAD_CONFLICTи перечитывает карточку.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — руководитель/админ агентства-владельца или текущий Executor с соответствующим
permission. Процесс нетерминален (сделка Status ∉ {Completed, Cancelled}; лид Status = Active). При назначении
ResponsibleUserId — активный агент агентства-владельца.
Постусловие (успех, назначение). Активный Responsible процесса — ResponsibleUserId (ровно один); прежний
куратор (если был) снят; денормализованный ResponsibleAgentUserId обновлён; Executor не изменён; Version++.
Опубликовано DealMembersChangedEvent / LeadParticipantsChangedEvent с полным активным составом и дельтой
ChangeType = ResponsibleChanged.
Постусловие (успех, снятие). ResponsibleAgentUserId = null, активный Responsible снят из состава;
Executor и его инвариант единственности не нарушены; опубликовано событие с ChangeType = ResponsibleChanged
(дельта Removed).
Постусловие (no-op). Назначение уже действующего куратора / снятие при отсутствующем — состав, денормализация и outbox не изменены; возвращается текущая карточка.
Постусловие (отказ). Ни состав, ни денормализация, ни события.
Окно eventual-consistency. Актор видит куратора сразу; синхронизация участников чата (messenger) и
уведомления (notifications) приходят с лагом (SLA консюмеров < 1 мин); порядок изменений per-process
гарантирован ключом DealId/LeadId.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | сделка — DealMembersChangedEvent → pin.crm.deal-members-changed.v1 (ChangeType = ResponsibleChanged); лид — LeadParticipantsChangedEvent → pin.crm.lead-participants-changed.v1 (ChangeType = ResponsibleChanged); outbox, key = DealId/LeadId |
| Внешние вызовы | при назначении — проверка активного членства кандидата в агентстве-владельце (реплика/справка identity agent-membership, timeout 2 c) |
| Проекции / поиск | денормализованный ResponsibleAgentUserId обновляется (индекс where not null); проекция поиска сделок не затрагивается (роль куратора в поиск не входит) |
| Уведомления | опосредованно: консюмеры deal-members-changed/lead-participants-changed — messenger реконсилирует участников чата, notifications уведомляет назначенного/снятого куратора |
Ошибки и failure modes
Главные гейты: процесс должен быть нетерминален (сделка Status ∉ {Completed, Cancelled} — иначе
CRM_DEAL_ALREADY_TERMINAL; лид Status = Active — иначе CRM_LEAD_NOT_ACTIVE), а назначаемый кандидат —
активный агент агентства-владельца (иначе CRM_PARTICIPANT_NOT_IN_AGENCY).
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат DealId/LeadId/ResponsibleUserId/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 | назначаемый ResponsibleUserId — не активный агент агентства-владельца | «Кандидат не состоит в агентстве» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) | «Сделка завершена и не редактируется» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | лид Expired/Archived | «Лид неактивен» | no retry |
CRM_DEAL_CONFLICT / CRM_LEAD_CONFLICT | 409 | конкурентная правка состава/устаревший RowVersion | «Данные изменены, обновите карточку» | retry (перечитать) |
Повторное назначение уже действующего Responsible или повторное снятие при отсутствующем кураторе — 200 с
текущей карточкой, без мутаций и событий (проверяется на шаге 2 алгоритма, до бизнес-правил).
Совместимость и наблюдаемость
- Контракт: route +
ResponsibleUserId/Reason/RowVersion(body) →ProcessMembershipModel. Новые optional-поля — additive; смена семантики роли — breaking. Новые значенияParticipantRole— additive (см. deal-members-changed.md). - Logs (без PII):
responsible reassigned(DealId/LeadId,FromResponsibleUserId → ToResponsibleUserId|null,ActorUserId;Reason/ФИО не пишутся); metricscrm_responsible_reassigned_total{kind, action}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path (назначение). Given нетерминальная сделка с
ExecutorA и агент C — активный агент агентства-владельца; When руководитель назначает C ответственным; Then активныйResponsible— C (ResponsibleAgentUserId = C),Executor= A не изменён, опубликованDealMembersChangedEvent(ResponsibleChanged),200. - Responsible ≠ Executor и Responsible = Executor. Given назначают
Responsible, отличного отExecutor— разрешено; When назначают тем же пользователем, что иExecutor— тоже разрешено (ограничения «≠ Executor» нет); Then в обоих случаях200. - Снятие. Given у процесса есть активный
Responsible; WhenResponsibleUserId = null; ThenResponsibleснят (ResponsibleAgentUserId = null),Executorне затронут, опубликовано событиеResponsibleChanged(дельтаRemoved). - Кандидат не из агентства. Given
ResponsibleUserId— агент другого агентства (или без активного членства); When назначение; Then422 CRM_PARTICIPANT_NOT_IN_AGENCY, состав не изменён. - No-op. Given назначают уже действующего куратора или снимают при отсутствующем; When
PUT; Then200с текущей карточкой, без мутаций и событий. - Терминальная сделка / неактивный лид. Given сделка
Completed/Cancelled→409 CRM_DEAL_ALREADY_TERMINAL; лидExpired/Archived→409 CRM_LEAD_NOT_ACTIVE. - Чужой актор. Given актор — рядовой со-агент; When операция; Then
403 CRM_DEAL_FORBIDDEN/CRM_LEAD_FORBIDDEN. - Гонка. Given параллельная правка состава / устаревший
RowVersion; When операция; Then409 CONFLICT; проигравший перечитывает карточку. - Downstream (eventual). Given успешное назначение/снятие; When проходит ≤ 1 мин; Then
messengerреконсилирует участников чата,notificationsуведомляет куратора.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): PUT /api/agent/crm/{deals|leads}/{id}/executor — Передать ведение (сменить исполнителя)
- Use Cases (4): UC-CRM-009. Передача ведения сделки (смена исполнителя), UC-CRM-010. Назначение/снятие ответственного (куратора) процесса — ответственный ≠ исполнитель, UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности (дом полей, инварианты состава): deal.md (INV-DEAL-3),
lead.md (INV-LEAD-7); таймлайн:
deal-timeline-event.md (
MemberAdded/MemberRemoved). - События: deal-members-changed.md,
lead-participants-changed.md (
ChangeType = ResponsibleChanged). - Членство агента: agent-membership.md. Смежная операция (смена исполнителя/получателя комиссии) — reassign-executor.md. Карта областей — README контекста.
- ADR: 0025 scoped-rbac, 0052 tenancy, 0071 crm-ownership-and-participant-model.