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 area | agent (JWT агента/куратора/руководителя агентства/Менеджера ПИН) |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/set-primary-investor.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (агент-исполнитель / куратор лида) / User (администратор-руководитель агентства) / User (Менеджер ПИН, platform-scope) |
| Auth policy / Permissions | crm.leads.manage |
| Scope (RBAC) | организация (агентство) — record-level по членству (ADR-0071); руководитель — весь TenantId (ADR-0025); Менеджер ПИН — платформа |
| Record-level | доступ = актор ∈ активные участники лида агентской роли (Executor/Responsible) или администратор/руководитель компании-владельца или Менеджер ПИН; иначе CRM_LEAD_FORBIDDEN |
| Tenant isolation | TenantId = AgencyCompanyId в каждом запросе; lead.TenantId обязан совпасть |
| Audit | смена ролей участников (UpdatedByUserId = actor), Version лида |
Вызвать операцию может только участник лида агентской роли (Executor/Responsible), администратор/
руководитель компании-владельца лида или Менеджер ПИН (platform-scope) в рамках своего TenantId; для всех
остальных — отказ 403 CRM_LEAD_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/leads/{leadId}/primary-investor |
| Success status | 200 (основной инвестор назначен, возвращается актуальный состав) |
| Idempotency header | не применяется — операция идемпотентна по цели: если UserId уже InvestorPrimary, повтор — no-op 200 |
| Correlation | сквозной trace через заголовок traceparent |
Отдельный idempotency-заголовок не используется: операция идемпотентна по цели — если UserId уже
InvestorPrimary, повторный вызов ничего не меняет, событие не публикуется, ответ — 200 с текущим составом.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = AgencyCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | Required | Record-level (членство) |
LeadId | route | Guid | Да | != Guid.Empty; существующий лид | — |
UserId | body | Guid | Да | != Guid.Empty; активный инвестор-участник лида (InvestorPrimary/InvestorSecondary) | — |
Модель ответа LeadParticipantsModel
Актуальный активный состав лида после смены основного (та же проекция, что у add-lead-participant.md).
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
LeadId | Guid | Да | lead.Id | Идентификатор лида. |
Participants | List<LeadParticipantModel> | Да | активные LeadParticipantEntity (RemovedAt is null) | Полный активный состав; ровно один с IsPrimary = true. |
ExecutorAgentUserId | Guid | Да | lead.ExecutorAgentUserId | Текущий исполнитель (не меняется этой операцией). |
ResponsibleAgentUserId | Guid? | Нет | lead.ResponsibleAgentUserId | Куратор (не меняется этой операцией). |
PrimaryInvestorUserId | Guid | Да | lead.PrimaryInvestorUserId | Обновлённый основной инвестор (= UserId). |
Version | long | Да | lead.Version | Для оптимистичной блокировки на клиенте. |
LeadParticipantModel — { UserId, Role, IsPrimary } (см. add-lead-participant.md).
Алгоритм
- Контекст и доступ. Операцию выполняет участник лида агентской роли / руководитель агентства / Менеджер
ПИН (permission
crm.leads.manage) в рамкахTenantId. - Проверка входа и предусловий.
- формат:
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).
- формат:
- Идемпотентность цели. Если
UserIdуже активныйInvestorPrimary— no-op: состав и производные не меняются, событие не публикуется, возвращается текущий состав200. - Guard активной пары (INV-CRM-001). Смена основного меняет производную
PrimaryInvestorUserId, а с ней — пару (ExecutorAgentUserId,PrimaryInvestorUserId). Если у пары (текущий исполнитель, новый основной) уже существует другой активный лид — отказCRM_LEAD_ACTIVE_PAIR_EXISTS(нельзя задвоить активную воронку пары). - Что читаем. Лид с активным составом (строка лида блокируется от гонки со сменой состава/передачей ведения).
- Бизнес-правила и переходы. Атомарно: прежний активный
InvestorPrimary→ рольInvestorSecondary(IsPrimary = false), целевой участник → рольInvestorPrimary(IsPrimary = true). Пересчитывается производнаяPrimaryInvestorUserId = UserId.ExecutorAgentUserId/ResponsibleAgentUserIdи агентский состав не меняются. Статус лида:Active → Active. Инвариант INV-LEAD-7 (ровно одинInvestorPrimary) сохранён на всём протяжении; лид не остаётся без основного инвестора (CRM_PRIMARY_INVESTOR_REQUIRED— guard самой операции: она всегда назначает ровно одного). - Что меняется. Обновляются роли двух участий (
crm.lead_participants), производнаяPrimaryInvestorUserId,UpdatedAt/Versionлида — атомарно;RowVersion→ при конфликтеCRM_LEAD_CONCURRENCY. - События. Публикуется
pin.crm.lead-participants-changed.v1(ChangeType = RoleChangedдля обоих инвесторов) атомарно: дельта смены ролей + полный активный состав. Консюмеры:messenger(реконсиляция состава чата лида — членство инвесторов не меняется),notifications. - Результат. Актуальный состав лида с обновлённым основным → HTTP
200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | pin.crm.lead-participants-changed.v1 (ChangeType = RoleChanged) — контракт lead-participants-changed.md |
| Внешние вызовы | none |
| Проекции / поиск | пересчёт PrimaryInvestorUserId (шапка лида в списках/карточке); членство инвесторов не меняется |
| Уведомления | notifications — по интеграционному событию (смена основного клиента) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | LeadId/UserId = Guid.Empty | «Проверьте корректность полей» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден по LeadId в tenant | «Лид не найден» | no retry |
CRM_LEAD_PARTICIPANT_NOT_FOUND | 404 | UserId не активный инвестор-участник лида | «Инвестор не найден в составе лида» | no retry (сначала добавить участника) |
CRM_LEAD_NOT_ACTIVE | 409 | Status != Active (INV-LEAD-2) | «Менять состав можно только у активного лида» | no retry |
CRM_LEAD_FORBIDDEN | 403 | актор не участник агентской роли / не руководитель / не Менеджер ПИН | «Недостаточно прав на изменение состава лида» | no retry |
CRM_LEAD_ACTIVE_PAIR_EXISTS | 409 | у пары (исполнитель, новый основной) уже есть активный лид (INV-CRM-001) | «Для этой пары уже есть активный лид» | no retry |
CRM_LEAD_CONCURRENCY | 409 | конфликт 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. Дано: активный лид с
InvestorPrimaryA иInvestorSecondaryB, актор — исполнитель. Когда: назначает основным 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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): DELETE /api/agent/crm/leads/{leadId}/participants/{userId} — Снять участника лида, POST /api/agent/crm/leads/{leadId}/participants — Добавить участника лида
- Use Cases (2): UC-CRM-011. Лид с несколькими инвесторами: назначение основного инвестора, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность-дом: 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-иерархия).