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

POST /api/agent/crm/leads/{leadId}/primary-investor — Назначить основного инвестора лида

Агент-исполнитель, куратор лида, руководитель агентства или Менеджер ПИН меняют основного инвестора у активного лида среди уже добавленных инвесторов-участников (InvestorPrimary / InvestorSecondary, единый ParticipantRole, ADR-0071). Основной инвестор ровно один (INV-LEAD-7): операция «поднимает» выбранного созаёмщика в InvestorPrimary, а прежний основной становится InvestorSecondary, и производная лида PrimaryInvestorUserId пересчитывается в том же изменении. Дом полей — lead.md (раздел «Участники лида»).

Назначение

Дать агенту-исполнителю / куратору / руководителю агентства (или Менеджеру ПИН) выбрать, кто из инвесторов-участников лида — основной клиент пары «исполнитель ↔ основной инвестор». Основной инвестор определяет уникальность активной пары (INV-CRM-001) и является «шапкой» лида. Операция меняет только признак основного среди уже добавленных инвесторов (нового участника не заводит — добавление через add-lead-participant.md). Публикует pin.crm.lead-participants-changed.v1 (ChangeType = RoleChanged) с дельтой и полным активным составом.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent (JWT агента/куратора/руководителя агентства/Менеджера ПИН)
Feature groupleads
Статусdraft
Документdocs/06-services/crm/api/agent/set-primary-investor.md

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

ПроверкаЗначение
Actor typeUser (агент-исполнитель / куратор лида) / User (администратор-руководитель агентства) / User (Менеджер ПИН, platform-scope)
Auth policy / Permissionscrm.leads.manage
Scope (RBAC)организация (агентство) — record-level по членству (ADR-0071); руководитель — весь TenantId (ADR-0025); Менеджер ПИН — платформа
Record-levelдоступ = актор ∈ активные участники лида агентской роли (Executor/Responsible) или администратор/руководитель компании-владельца или Менеджер ПИН; иначе CRM_LEAD_FORBIDDEN
Tenant isolationTenantId = AgencyCompanyId в каждом запросе; lead.TenantId обязан совпасть
Auditсмена ролей участников (UpdatedByUserId = actor), Version лида
Доступ

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/leads/{leadId}/primary-investor
Success status200 (основной инвестор назначен, возвращается актуальный состав)
Idempotency headerне применяется — операция идемпотентна по цели: если UserId уже InvestorPrimary, повтор — no-op 200
Correlationсквозной trace через заголовок traceparent
Идемпотентность

Отдельный idempotency-заголовок не используется: операция идемпотентна по цели — если UserId уже InvestorPrimary, повторный вызов ничего не меняет, событие не публикуется, ответ — 200 с текущим составом.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = AgencyCompanyIdTenant isolation
ActorUserIdidentity claimsGuidДаRequiredRecord-level (членство)
LeadIdrouteGuidДа!= Guid.Empty; существующий лид
UserIdbodyGuidДа!= Guid.Empty; активный инвестор-участник лида (InvestorPrimary/InvestorSecondary)

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

Актуальный активный состав лида после смены основного (та же проекция, что у add-lead-participant.md).

ПолеТипОбязательностьИсточникОписание
LeadIdGuidДаlead.IdИдентификатор лида.
ParticipantsList<LeadParticipantModel>Даактивные LeadParticipantEntity (RemovedAt is null)Полный активный состав; ровно один с IsPrimary = true.
ExecutorAgentUserIdGuidДаlead.ExecutorAgentUserIdТекущий исполнитель (не меняется этой операцией).
ResponsibleAgentUserIdGuid?Нетlead.ResponsibleAgentUserIdКуратор (не меняется этой операцией).
PrimaryInvestorUserIdGuidДаlead.PrimaryInvestorUserIdОбновлённый основной инвестор (= UserId).
VersionlongДаlead.VersionДля оптимистичной блокировки на клиенте.

LeadParticipantModel{ UserId, Role, IsPrimary } (см. add-lead-participant.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет участник лида агентской роли / руководитель агентства / Менеджер ПИН (permission crm.leads.manage) в рамках TenantId.
  2. Проверка входа и предусловий.
    • формат: LeadId != Guid.Empty, UserId != Guid.Empty;
    • лид существует в TenantId — иначе CRM_LEAD_NOT_FOUND;
    • состояние: lead.Status == Active (INV-LEAD-2) — иначе CRM_LEAD_NOT_ACTIVE;
    • record-level (членство, ADR-0071) — иначе CRM_LEAD_FORBIDDEN;
    • UserId — активный инвестор-участник лида (роль InvestorPrimary или InvestorSecondary) — иначе CRM_LEAD_PARTICIPANT_NOT_FOUND (если не инвестор/не участник — сначала добавить через add-lead-participant.md).
  3. Идемпотентность цели. Если UserId уже активный InvestorPrimary — no-op: состав и производные не меняются, событие не публикуется, возвращается текущий состав 200.
  4. Guard активной пары (INV-CRM-001). Смена основного меняет производную PrimaryInvestorUserId, а с ней — пару (ExecutorAgentUserId,PrimaryInvestorUserId). Если у пары (текущий исполнитель, новый основной) уже существует другой активный лид — отказ CRM_LEAD_ACTIVE_PAIR_EXISTS (нельзя задвоить активную воронку пары).
  5. Что читаем. Лид с активным составом (строка лида блокируется от гонки со сменой состава/передачей ведения).
  6. Бизнес-правила и переходы. Атомарно: прежний активный InvestorPrimary → роль InvestorSecondary (IsPrimary = false), целевой участник → роль InvestorPrimary (IsPrimary = true). Пересчитывается производная PrimaryInvestorUserId = UserId. ExecutorAgentUserId/ResponsibleAgentUserId и агентский состав не меняются. Статус лида: Active → Active. Инвариант INV-LEAD-7 (ровно один InvestorPrimary) сохранён на всём протяжении; лид не остаётся без основного инвестора (CRM_PRIMARY_INVESTOR_REQUIRED — guard самой операции: она всегда назначает ровно одного).
  7. Что меняется. Обновляются роли двух участий (crm.lead_participants), производная PrimaryInvestorUserId, UpdatedAt/Version лида — атомарно; RowVersion → при конфликте CRM_LEAD_CONCURRENCY.
  8. События. Публикуется pin.crm.lead-participants-changed.v1 (ChangeType = RoleChanged для обоих инвесторов) атомарно: дельта смены ролей + полный активный состав. Консюмеры: messenger (реконсиляция состава чата лида — членство инвесторов не меняется), notifications.
  9. Результат. Актуальный состав лида с обновлённым основным → HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияpin.crm.lead-participants-changed.v1 (ChangeType = RoleChanged) — контракт lead-participants-changed.md
Внешние вызовыnone
Проекции / поискпересчёт PrimaryInvestorUserId (шапка лида в списках/карточке); членство инвесторов не меняется
Уведомленияnotifications — по интеграционному событию (смена основного клиента)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400LeadId/UserId = Guid.Empty«Проверьте корректность полей»no retry
CRM_LEAD_NOT_FOUND404лид не найден по LeadId в tenant«Лид не найден»no retry
CRM_LEAD_PARTICIPANT_NOT_FOUND404UserId не активный инвестор-участник лида«Инвестор не найден в составе лида»no retry (сначала добавить участника)
CRM_LEAD_NOT_ACTIVE409Status != Active (INV-LEAD-2)«Менять состав можно только у активного лида»no retry
CRM_LEAD_FORBIDDEN403актор не участник агентской роли / не руководитель / не Менеджер ПИН«Недостаточно прав на изменение состава лида»no retry
CRM_LEAD_ACTIVE_PAIR_EXISTS409у пары (исполнитель, новый основной) уже есть активный лид (INV-CRM-001)«Для этой пары уже есть активный лид»no retry
CRM_LEAD_CONCURRENCY409конфликт RowVersion (параллельная смена состава/передача ведения)«Лид изменён, обновите и повторите»retry (перечитать)
Ошибки

CRM_PRIMARY_INVESTOR_REQUIRED (INV-LEAD-7) — инвариант «ровно один основной инвестор»: эта операция его поддерживает (всегда назначает единственного основного); ошибка возникает не здесь, а при попытке снять активного InvestorPrimary без назначения замены (remove-lead-participant.md).

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

Предусловия (истинны до сценария):

  • актор авторизован, имеет permission crm.leads.manage, является участником агентской роли лида / руководителем агентства / Менеджером ПИН;
  • лид существует в TenantId актора и находится в статусе Active (INV-LEAD-2);
  • UserId — активный инвестор-участник лида (InvestorPrimary/InvestorSecondary);
  • у пары (текущий исполнитель, новый основной) нет другого активного лида (INV-CRM-001).

Постусловия (гарантированы после успеха):

  • целевой участник имеет роль InvestorPrimary (IsPrimary = true); прежний основной — InvestorSecondary (IsPrimary = false); среди активных ровно один InvestorPrimary (INV-LEAD-7);
  • производная PrimaryInvestorUserId = UserId; ExecutorAgentUserId/ResponsibleAgentUserId не изменены;
  • Version лида инкрементирован, статус остаётся Active; членство инвесторов в лиде не менялось (менялась только роль);
  • опубликован pin.crm.lead-participants-changed.v1 (RoleChanged) с полным активным составом;
  • при вызове с уже основным UserId — состояние не изменилось, событие не опубликовано (идемпотентный no-op);
  • заявки на фиксацию и сделки лида не изменены; Origin-атрибуция не тронута.

Acceptance Criteria

  • Happy path. Дано: активный лид с InvestorPrimary A и InvestorSecondary B, актор — исполнитель. Когда: назначает основным B. Тогда: 200, B — InvestorPrimary, A — InvestorSecondary, PrimaryInvestorUserId = B, Version +1, опубликован lead-participants-changed (RoleChanged).
  • Идемпотентный повтор. Дано: A уже InvestorPrimary. Когда: назначают основным A. Тогда: 200, состав без изменений, событие не публикуется.
  • Не инвестор / не участник. Дано: UserId не в составе инвесторов лида (агент или посторонний). Когда: назначение. Тогда: 404 CRM_LEAD_PARTICIPANT_NOT_FOUND (сначала добавить участника).
  • Конфликт активной пары. Дано: у пары (исполнитель, новый основной) уже есть другой активный лид. Когда: назначение. Тогда: 409 CRM_LEAD_ACTIVE_PAIR_EXISTS, состав не меняется.
  • Чужой лид. Дано: актор не участник агентской роли / не руководитель / не Менеджер ПИН. Когда: назначение. Тогда: 403 CRM_LEAD_FORBIDDEN.
  • Лид не активен. Дано: лид Expired/Archived. Когда: назначение. Тогда: 409 CRM_LEAD_NOT_ACTIVE.
  • Конкурентная смена состава. Дано: состав параллельно изменён. Когда: отправка с устаревшей версией. Тогда: 409 CRM_LEAD_CONCURRENCY.

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

  • Инвариант «ровно один InvestorPrimary» (INV-LEAD-7) поддерживается атомарной сменой ролей пары инвесторов. ParticipantRole — additive (неизвестное значение у потребителя — строкой + warning).
  • Logs: lead-primary-investor set (LeadId, tenant, actor; прежний/новый основной — только id); metrics: crm_lead_set_primary_investor_total{result}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

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

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

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

  • Сущность-дом: lead.md (LeadParticipantEntity, INV-LEAD-7, INV-CRM-001).
  • Смежные операции состава: add-lead-participant.md, remove-lead-participant.md.
  • Событие: lead-participants-changed.md; аналог на сделке — deal.md (единая модель участников).
  • ADR-0071 (владение процессами, ParticipantRole, видимость по составу), ADR-0025 (scope-иерархия).