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

PUT /api/agent/crm/deals/{dealId}/members/{memberId}/role — Сменить роль участника сделки

Агент (или руководитель агентства / Менеджер ПИН) меняет роль активного участника сделки. Метод покрывает как обычную смену роли (например Assistant → CoAgent), так и передачу ведения — назначение участника исполнителем (Executor) или основным инвестором (InvestorPrimary) с атомарным понижением прежнего носителя singleton-роли. Дом полей состава, ролей и инвариантов — в deal.md (DealMemberEntity, ParticipantRole, INV-DEAL-3, INV-DEAL-10).

Назначение

Дать агентской стороне перераспределить роли внутри сделки: повысить со-агента до исполнителя (передача ведения — новый получатель комиссии), назначить/сменить куратора (Responsible), сделать соинвестора основным (InvestorPrimary) или изменить нефинансовую роль сопровождения. Singleton-роли (Executor, InvestorPrimary) в сделке всегда ровно по одной активной: назначение новой роли-синглтона выполняется как атомарная передача — прежний носитель понижается в той же операции, и процесс ни на миг не остаётся без исполнителя (INV-DEAL-3).

Ограничения

Историческая атрибуция (OriginAgentUserId/OriginAgencyCompanyId) при передаче ведения не меняется (INV-DEAL-10). Добавление и снятие участников выполняется отдельными операциями (add-deal-participant.md, remove-deal-participant.md), а не этим методом.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/agent/change-participant-role.md

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

ПроверкаЗначение
Actor typeUser — активный участник сделки (агентская сторона), руководитель компании-владельца или Менеджер ПИН
Auth policy / Permissionscrm.deals.manage (агентская сторона); Менеджер ПИН — platform-scope
Scope (RBAC)организация (агентство-владелец); сделка живёт в tenant застройщика (кросс-tenant по ADR-0052)
Record-levelдоступ = актор ∈ активные участники сделки (любая роль ParticipantRole) ИЛИ администратор/руководитель компании-владельца (агентство активного Executor по scope-иерархии ADR-0025; застройщик — свой TenantId) ИЛИ Менеджер ПИН (platform-scope, обход record-level)
Tenant isolationСделка TenantId = DeveloperCompanyId (владелец — застройщик, ADR-0052); агентская сторона идёт по членству
Auditupdate состава: Role целевого участия (при передаче ведения — понижение прежнего носителя); при передаче — денормализация ExecutorAgentUserId/PrimaryInvestorUserId/ResponsibleAgentUserId; Version++ сделки
Доступ

Выполнить может активный участник сделки (любая роль ParticipantRole), администратор/руководитель компании-владельца (агентство активного Executor по scope-иерархии ADR-0025; застройщик — свой TenantId) или Менеджер ПИН (platform-scope, обход record-level).

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/agent/crm/deals/{dealId}/members/{memberId}/role
Success status200 (обновлённое участие)
Idempotency headerне применяется — повтор той же роли = no-op (участие уже в целевой роли), гонка ловится конкурентной проверкой версии
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty; существующая сделка, доступная актору по членству
MemberIdrouteGuidДа!= Guid.Empty; активное участие (RemovedAt is null) этой сделки
NewRolebodystring (enum ParticipantRole)Даодно из Executor / Responsible / CoAgent / Assistant / Lawyer / InvestorPrimary / InvestorSecondary; SellManager/PinManager через агентский эндпоинт не назначаются → CRM_PARTICIPANT_ROLE_FORBIDDEN
RowVersionbodyuint?Нетнаблюдаемая версия карточки для оптимистической блокировки
ActorUserIdidentity claimsGuidДаиз JWT
ActorCompanyIdidentity claimsGuidДакомпания актора — record-level предикат
IsCompanyManageridentity claimsboolДаадминистратор/руководитель компании-владельца
IsPinManageridentity claimsboolДаplatform-scope Менеджера ПИН (обход record-level)

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

Участие после смены роли (дом полей — deal.md; ниже — отдаваемая дельта).

ПолеТипОбязательностьИсточникMaskingОписание
MemberIdGuidДаDealMemberEntity.IdИдентификатор участия.
DealIdGuidДаDealMemberEntity.DealIdСделка.
UserIdGuidДаDealMemberEntity.UserIdПользователь-участник.
Rolestring (enum ParticipantRole)ДаDealMemberEntity.Role (после смены)Новая роль участника.
PreviousExecutorUserIdGuid?Нетпонижённый прежний Executor (только при передаче ведения)Кто был исполнителем до передачи (для отражения в UI).
VersionlongДаDealEntity.VersionВерсия сделки после изменения.

Алгоритм

  1. Контекст и доступ. Операцию выполняет активный участник сделки (permission crm.deals.manage), руководитель компании-владельца или Менеджер ПИН (platform-scope).
  2. Проверка входа и предусловий.
    • формат: DealId/MemberId != Guid.Empty; NewRole задан и из enum ParticipantRole — иначе ошибка валидации;
    • NewRole назначается через агентский эндпоинт (не SellManager/PinManager) — иначе CRM_PARTICIPANT_ROLE_FORBIDDEN;
    • сделка существует и доступна актору по членству/руководству/ПИН — иначе CRM_DEAL_NOT_FOUND (404);
    • сделка нетерминальна (Status ∉ {Completed, Cancelled}, INV-DEAL-9) — иначе CRM_DEAL_ALREADY_TERMINAL;
    • участие MemberId существует, принадлежит сделке и активно (RemovedAt is null) — иначе CRM_DEAL_MEMBER_NOT_FOUND (404).
  3. Что читаем. Сделку (доступ/статус, текущие носители singleton-ролей), целевое участие (MemberId, текущая роль) и — при назначении роли-синглтона — действующего носителя этой роли.
  4. Бизнес-правила и переходы (INV-DEAL-3).
    • No-op. Если NewRole == member.Role → изменений нет, возвращается текущее участие (идемпотентный повтор); мутаций и событий нет.
    • Смена на несинглтон (CoAgent/Assistant/Lawyer/InvestorSecondary): роль участия меняется напрямую; если участник был носителем singleton-роли (Executor/InvestorPrimary) и понижается без назначения преемника — операция запрещена (CRM_LAST_EXECUTOR_REQUIRED для Executor; понижение единственного InvestorPrimary без назначения нового — CRM_PARTICIPANT_ROLE_FORBIDDEN). Событие — RoleChanged.
    • Передача ведения (NewRole = Executor): целевой участник становится Executor; прежний активный Executor в той же операции понижается до CoAgent (не остаётся вторым Executor) → ExecutorChanged. Денормализованный ExecutorAgentUserId = member.UserId. OriginAgentUserId/OriginAgencyCompanyId не меняются (INV-DEAL-10). Процесс ни на миг не остаётся без исполнителя.
    • Смена основного инвестора (NewRole = InvestorPrimary): целевой участник становится InvestorPrimary; прежний InvestorPrimary понижается до InvestorSecondary в той же операции; PrimaryInvestorUserId = member.UserId. Событие — RoleChanged (полный ActiveMembers несёт актуальный синглтон).
    • Назначение/смена куратора (NewRole = Responsible): если действующий Responsible есть — понижается (Responsible опционален, ≤1); ResponsibleAgentUserId = member.UserIdResponsibleChanged.
    • Version++, UpdatedByUserId = actor, UpdatedAt = now.
  5. Что меняется. Роль целевого участия (и, при передаче, роль прежнего носителя singleton-роли) + денормализованные скаляры сделки (ExecutorAgentUserId/PrimaryInvestorUserId/ResponsibleAgentUserId) — в одном согласованном изменении с Version++ (при желании — запись в таймлайн MemberRoleChanged, deal-timeline-event.md).
  6. События. DealMembersChangedEvent → топик pin.crm.deal-members-changed.v1 (ContractVersion = 1, AggregateId = DealId) с дельтой Members (при передаче ведения — две записи: понижение прежнего носителя
    • назначение новому) и ChangeType (RoleChanged/ExecutorChanged/ResponsibleChanged) плюс полный активный состав ActiveMembers после изменения — transactional outbox; контракт — deal-members-changed.md.
  7. Результат. Участие после смены роли → HTTP 200. Конкурентная гонка (устаревший RowVersion или параллельная смена состава) → CRM_DEAL_CONFLICT (409); клиент перечитывает состав.

Диаграмма

Предусловия и постусловия

Предусловия. Актор — активный участник сделки / руководитель компании-владельца / Менеджер ПИН с crm.deals.manage. Сделка существует, доступна актору и нетерминальна (INV-DEAL-9). MemberId — активное участие этой сделки. NewRole назначается через агента (не SellManager/PinManager).

Постусловие (успех, не no-op). Роль целевого участия = NewRole. При передаче ведения (Executor) прежний исполнитель понижен до CoAgent, ExecutorAgentUserId = member.UserId, Origin-атрибуция не изменена (INV-DEAL-10). При InvestorPrimary прежний основной инвестор понижен до InvestorSecondary, PrimaryInvestorUserId обновлён. При Responsible действующий куратор понижен, ResponsibleAgentUserId обновлён. Инварианты INV-DEAL-3 сохранены (ровно один активный Executor и InvestorPrimary). Version инкрементирован. Опубликован DealMembersChangedEvent (pin.crm.deal-members-changed.v1, key DealId) с соответствующим ChangeType и полным ActiveMembers.

Eventual consistency

Новая раскладка ролей видна в ответе сразу; чат (messenger), уведомления (notifications) и — при передаче ведения — снятие доступа уходящего исполнителя обновляются с лагом консюмеров (< 1 мин).

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

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

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

ТипДетали
Интеграционные событияDealMembersChangedEventpin.crm.deal-members-changed.v1 (RoleChanged/ExecutorChanged/ResponsibleChanged + полный ActiveMembers), outbox
Уведомленияопосредованно: консюмер deal-members-changed в notifications (новому исполнителю/куратору); messenger — обновление ролей участников чата + системное сообщение о передаче ведения

Ошибки и failure modes

Ошибки

Понижение единственного активного Executor без назначения преемника в той же операции запрещено — 409 CRM_LAST_EXECUTOR_REQUIRED (INV-DEAL-3): передача ведения обязана оставаться атомарной. Понижение единственного InvestorPrimary без назначения нового блокируется 422 CRM_PARTICIPANT_ROLE_FORBIDDEN.

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId/MemberId = Guid.Empty или NewRole вне enum«Проверьте корректность запроса»no retry
CRM_PARTICIPANT_ROLE_FORBIDDEN422NewRole = SellManager/PinManager; попытка понизить единственного InvestorPrimary без назначения нового«Эта роль назначается другим способом»no retry
CRM_DEAL_NOT_FOUND404сделки нет или она недоступна актору (record-level скрывает существование)«Сделка не найдена»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9)«Сделка завершена и не редактируется»no retry
CRM_DEAL_MEMBER_NOT_FOUND404участия нет, оно чужой сделки или снято«Участник не найден»no retry
CRM_LAST_EXECUTOR_REQUIRED409понижение единственного активного Executor без назначения преемника в той же операции (INV-DEAL-3)«Нельзя оставить сделку без исполнителя — передайте ведение»no retry
CRM_DEAL_CONFLICT409конкурентная гонка (устаревший RowVersion / параллельная смена состава)«Состав сделки изменён, обновите данные»retry (перечитать состав)

Совместимость и наблюдаемость

  • Контракт: route + NewRole/RowVersion (body) → DealMemberModel. Новые optional-поля — additive; смена маршрута/семантики передачи ведения — breaking (migration + rollout). Новые значения ParticipantRole — additive (см. deal-members-changed.md).
  • Logs: deal-member role-changed / executor-reassigned (DealId, TenantId, MemberId, FromRoleNewRole, ActorUserId; ФИО/телефон не пишутся); metrics crm_deal_member_role_changed_total{change_type}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Смена нефинансовой роли. Given активный Assistant; When PUT c NewRole = CoAgent; Then 200, роль обновлена, событие ChangeType = RoleChanged, полный ActiveMembers.
  • Передача ведения. Given сделка с исполнителем A и со-агентом B; When PUT для B с NewRole = Executor; Then 200, B — Executor, A понижен до CoAgent, ExecutorAgentUserId = B, событие ChangeType = ExecutorChanged, в ActiveMembers ровно один Executor; OriginAgentUserId/ OriginAgencyCompanyId не изменены (INV-DEAL-10).
  • Смена основного инвестора. Given InvestorPrimary P и InvestorSecondary S; When PUT для S с NewRole = InvestorPrimary; Then S — InvestorPrimary, P понижен до InvestorSecondary, PrimaryInvestorUserId = S, в ActiveMembers ровно один InvestorPrimary.
  • Назначение куратора. Given без активного Responsible; When PUT c NewRole = Responsible; Then 200, ResponsibleAgentUserId задан, событие ChangeType = ResponsibleChanged.
  • Нельзя понизить последнего исполнителя. Given единственный активный Executor; When PUT c NewRole = CoAgent для него (без назначения преемника); Then 409 CRM_LAST_EXECUTOR_REQUIRED.
  • Недопустимая роль. Given NewRole = SellManager/PinManager; When PUT; Then 422 CRM_PARTICIPANT_ROLE_FORBIDDEN.
  • No-op. Given NewRole == member.Role; When PUT; Then 200 с текущим участием, без мутаций и событий.
  • Терминальная сделка. Given сделка Completed/Cancelled; When PUT; Then 409 CRM_DEAL_ALREADY_TERMINAL.
  • Гонка. Given параллельная смена состава или устаревший RowVersion; When PUT; Then 409 CRM_DEAL_CONFLICT; проигравший перечитывает состав.
  • Downstream (eventual). Given успешная передача ведения; When проходит < 1 мин; Then роли участников чата обновлены (messenger), новый исполнитель уведомлён (notifications), доступ уходящего исполнителя снят вместе с его понижением.

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

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

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