POST /api/agent/crm/deals/{dealId}/members — Добавить участника сделки
Агент (или руководитель агентства / Менеджер ПИН) добавляет в сделку дополнительного участника — со-агента,
ассистента, юриста сопровождения или соинвестора. Расширяет состав сделки без смены исполнителя и основного
инвестора: новый участник получает доступ к сделке (по членству) и попадает в чат сделки (messenger). Дом
полей состава участников, ролей и инвариантов — в deal.md (DealMemberEntity,
ParticipantRole, INV-DEAL-3).
Назначение
Дать агентской стороне подключить к сделке нефинансовые роли (CoAgent/Assistant/Lawyer) и
дополнительных инвесторов (InvestorSecondary) — созаёмщиков/соинвесторов. Операция добавляет одного
активного участника, поддерживает уникальность пользователя в сделке и поднимает событие для синхронизации
участников чата. Смена исполнителя (Executor), куратора (Responsible) и основного инвестора
(InvestorPrimary) выполняется отдельной операцией смены роли (change-participant-role.md),
а не этим методом; стороны застройщика (SellManager) и авто-участник ПИН (PinManager) через агентский
эндпоинт не добавляются.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/add-deal-participant.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — активный участник сделки (агентская сторона), руководитель компании-владельца или Менеджер ПИН |
| Auth policy / Permissions | crm.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); агентская сторона идёт по членству, а не по авто-tenant-фильтру |
| Audit | update состава: новая строка DealMemberEntity (AddedByUserId = actor, CreatedAt = now); Version++ сделки |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/deals/{dealId}/members |
| Success status | 201 (участник добавлен) |
| Idempotency header | не применяется — естественный дедуп по уникальности (DealId, UserId) среди активных: повтор того же пользователя → CRM_DUPLICATE_PARTICIPANT |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route | Guid | Да | != Guid.Empty; существующая сделка, доступная актору по членству | — |
UserId | body | Guid | Да | != Guid.Empty; существует в реплике identity; не совпадает с активным участником сделки (уникальность) | — |
Role | body | string (enum ParticipantRole) | Да | одно из CoAgent / Assistant / Lawyer / InvestorSecondary; прочие роли через этот эндпоинт не добавляются → CRM_PARTICIPANT_ROLE_FORBIDDEN | — |
ActorUserId | identity claims | Guid | Да | из JWT | — |
ActorCompanyId | identity claims | Guid | Да | компания актора — record-level предикат (агентство-владелец / застройщик) | — |
IsCompanyManager | identity claims | bool | Да | администратор/руководитель компании-владельца (доступ на сделки своей компании) | — |
IsPinManager | identity claims | bool | Да | platform-scope Менеджера ПИН (обход record-level) | — |
Модель ответа DealMemberModel
Добавленный участник (дом полей — deal.md, DealMemberEntity; ниже — отдаваемая дельта).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
MemberId | Guid | Да | DealMemberEntity.Id | — | Идентификатор участия (для последующего удаления/смены роли). |
DealId | Guid | Да | DealMemberEntity.DealId | — | Сделка. |
UserId | Guid | Да | DealMemberEntity.UserId | — | Пользователь-участник. |
Role | string (enum ParticipantRole) | Да | DealMemberEntity.Role | — | Назначенная роль. |
AddedByUserId | Guid | Да | DealMemberEntity.AddedByUserId | — | Кто добавил (актор). |
CreatedAt | DateTimeOffset | Да | DealMemberEntity.CreatedAt | — | Момент добавления. |
ФИО/телефон участника в ответ не входят — отображаемое имя UI резолвит по UserId из реплики identity.
Алгоритм
- Контекст и доступ. Операцию выполняет активный участник сделки (permission
crm.deals.manage), руководитель компании-владельца или Менеджер ПИН (platform-scope). - Проверка входа и предусловий.
- формат:
DealId/UserId!= Guid.Empty;Roleзадан и из enumParticipantRole— иначе ошибка валидации; - роль допустима для добавления через агентский эндпоинт (
CoAgent/Assistant/Lawyer/InvestorSecondary) — иначеCRM_PARTICIPANT_ROLE_FORBIDDEN(Executor/Responsible/InvestorPrimary— через change-participant-role.md;SellManager/PinManager— не через агента); - сделка существует и доступна актору по членству/руководству/ПИН — иначе
CRM_DEAL_NOT_FOUND(404, существование чужих сделок не раскрывается); - сделка нетерминальна (
Status ∉ {Completed, Cancelled}, INV-DEAL-9) — иначеCRM_DEAL_ALREADY_TERMINAL; UserIdесть в репликеidentity— иначеCRM_USER_NOT_FOUND;UserIdне является активным участником сделки (RemovedAt is null, uniqueux_deal_members_deal_user) — иначеCRM_DUPLICATE_PARTICIPANT.
- формат:
- Что читаем. Сделку (для проверки доступа/статуса и текущего активного состава) и наличие пользователя в
реплике
identity. - Бизнес-правила и переходы (INV-DEAL-3). Роли
CoAgent/Assistant/Lawyer/InvestorSecondaryдопускают произвольное число активных участников — конфликта singleton-ролей (Executor/InvestorPrimary) здесь не возникает. Добавляется активный участник (RemovedAt = null,AddedByUserId = actor,CreatedAt = now); денормализованные скаляры сделки (ExecutorAgentUserId/PrimaryInvestorUserId) не затрагиваются.Version++. - Что меняется. Появляется строка
DealMemberEntity— в одном согласованном изменении сVersion++сделки (при желании — запись в таймлайнMemberAdded, deal-timeline-event.md). - События.
DealMembersChangedEvent→ топикpin.crm.deal-members-changed.v1(ContractVersion = 1,AggregateId = DealId) с дельтойMembers = [{UserId, Role, ChangeType = Added}]и полным активным составомActiveMembersпосле изменения — transactional outbox; контракт — deal-members-changed.md. - Результат. Карточка добавленного участника → HTTP
201.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — активный участник сделки / руководитель компании-владельца / Менеджер ПИН с
crm.deals.manage. Сделка существует, доступна актору и нетерминальна (INV-DEAL-9). UserId существует в
identity и ещё не является активным участником сделки. Роль — одна из добавляемых через агента
(CoAgent/Assistant/Lawyer/InvestorSecondary).
Постусловие (успех). Добавлена активная DealMemberEntity (RemovedAt = null, AddedByUserId = actor);
Version сделки инкрементирован. Опубликован DealMembersChangedEvent (pin.crm.deal-members-changed.v1,
key DealId) с ChangeType = Added и полным ActiveMembers. Окно eventual-consistency: участник виден в
сделке сразу, но в чате сделки (messenger) и в уведомлениях (notifications) — с лагом консюмеров (< 1 мин).
Постусловие (отказ). Ни строки участника, ни изменения Version, ни интеграционного события.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | DealMembersChangedEvent → pin.crm.deal-members-changed.v1 (ChangeType = Added + полный ActiveMembers), outbox |
| Уведомления | опосредованно: консюмер deal-members-changed в notifications (уведомление добавленному участнику); messenger — приглашение в чат сделки |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId/UserId = Guid.Empty или Role вне enum | «Проверьте корректность запроса» | no retry |
CRM_PARTICIPANT_ROLE_FORBIDDEN | 422 | роль не добавляется через агентский эндпоинт (Executor/Responsible/InvestorPrimary/SellManager/PinManager) | «Эта роль назначается другим способом» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки нет или она недоступна актору (record-level скрывает существование) | «Сделка не найдена» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) | «Сделка завершена и не редактируется» | no retry |
CRM_USER_NOT_FOUND | 404 | UserId не найден в реплике identity | «Пользователь не найден» | no retry |
CRM_DUPLICATE_PARTICIPANT | 409 | UserId уже активный участник сделки (INV-DEAL-3, unique ux_deal_members_deal_user) | «Пользователь уже участвует в сделке» | no retry |
Совместимость и наблюдаемость
- Контракт: route +
UserId/Role(body) →DealMemberModel. Новые optional-поля — additive; смена маршрута/семантики — breaking (migration + rollout). Новые значенияParticipantRole— additive (см. deal-members-changed.md). - Logs:
deal-member added(DealId,TenantId,MemberId,Role,ActorUserId; ФИО/телефон не пишутся); metricscrm_deal_member_added_total{role}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given нетерминальная доступная сделка и новый
UserId; WhenPOSTс рольюCoAgent; Then201, добавлена активнаяDealMemberEntity,Version++, опубликованdeal-members-changed(ChangeType = Added, полныйActiveMembers). - Соинвестор. Given сделка с одним
InvestorPrimary; When добавляютInvestorSecondary; Then201(произвольное числоInvestorSecondary, singleton-инвариант не нарушен). - Дубль участника. Given
UserIdуже активный участник; WhenPOST; Then409 CRM_DUPLICATE_PARTICIPANT. - Недопустимая роль. Given
Role = Executor/InvestorPrimary/SellManager/PinManager; WhenPOST; Then422 CRM_PARTICIPANT_ROLE_FORBIDDEN(эти роли — через смену роли/передачу ведения или системно). - Терминальная сделка. Given сделка
Completed/Cancelled; WhenPOST; Then409 CRM_DEAL_ALREADY_TERMINAL. - Нет доступа. Given сделка, где актор не участник и не руководитель/ПИН; When
POST; Then404 CRM_DEAL_NOT_FOUND(существование не раскрывается). - Downstream (eventual). Given успешное добавление; When проходит < 1 мин; Then участник появляется в чате
сделки (
messenger) и получает уведомление (notifications).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): DELETE /api/agent/crm/deals/{dealId}/members/{memberId} — Удалить участника сделки, PUT /api/agent/crm/deals/{dealId}/members/{memberId}/role — Сменить роль участника сделки
- Use Cases (4): UC-CRM-007. Добавление участника в сделку, UC-CRM-008. Снятие участника со сделки, UC-CRM-009. Передача ведения сделки (смена исполнителя), US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей
DealMemberEntity,ParticipantRole, INV-DEAL-3): deal.md; таймлайн: deal-timeline-event.md. - Событие: deal-members-changed.md (
pin.crm.deal-members-changed.v1). - Соседние операции состава: remove-deal-participant.md, change-participant-role.md; создание сделки и начальный состав — create-deal.md.
- ADR: 0052 tenancy, 0055 мини-мессенджер, 0071 участники и владелец сделки.