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

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 areaagent
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/agent/add-deal-participant.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); агентская сторона идёт по членству, а не по авто-tenant-фильтру
Auditupdate состава: новая строка DealMemberEntity (AddedByUserId = actor, CreatedAt = now); Version++ сделки

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/deals/{dealId}/members
Success status201 (участник добавлен)
Idempotency headerне применяется — естественный дедуп по уникальности (DealId, UserId) среди активных: повтор того же пользователя → CRM_DUPLICATE_PARTICIPANT
Correlationсквозной trace через заголовок traceparent

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

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

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

Добавленный участник (дом полей — deal.md, DealMemberEntity; ниже — отдаваемая дельта).

ПолеТипОбязательностьИсточникMaskingОписание
MemberIdGuidДаDealMemberEntity.IdИдентификатор участия (для последующего удаления/смены роли).
DealIdGuidДаDealMemberEntity.DealIdСделка.
UserIdGuidДаDealMemberEntity.UserIdПользователь-участник.
Rolestring (enum ParticipantRole)ДаDealMemberEntity.RoleНазначенная роль.
AddedByUserIdGuidДаDealMemberEntity.AddedByUserIdКто добавил (актор).
CreatedAtDateTimeOffsetДаDealMemberEntity.CreatedAtМомент добавления.

ФИО/телефон участника в ответ не входят — отображаемое имя UI резолвит по UserId из реплики identity.

Алгоритм

  1. Контекст и доступ. Операцию выполняет активный участник сделки (permission crm.deals.manage), руководитель компании-владельца или Менеджер ПИН (platform-scope).
  2. Проверка входа и предусловий.
    • формат: DealId/UserId != Guid.Empty; Role задан и из enum ParticipantRole — иначе ошибка валидации;
    • роль допустима для добавления через агентский эндпоинт (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, unique ux_deal_members_deal_user) — иначе CRM_DUPLICATE_PARTICIPANT.
  3. Что читаем. Сделку (для проверки доступа/статуса и текущего активного состава) и наличие пользователя в реплике identity.
  4. Бизнес-правила и переходы (INV-DEAL-3). Роли CoAgent/Assistant/Lawyer/InvestorSecondary допускают произвольное число активных участников — конфликта singleton-ролей (Executor/InvestorPrimary) здесь не возникает. Добавляется активный участник (RemovedAt = null, AddedByUserId = actor, CreatedAt = now); денормализованные скаляры сделки (ExecutorAgentUserId/PrimaryInvestorUserId) не затрагиваются. Version++.
  5. Что меняется. Появляется строка DealMemberEntity — в одном согласованном изменении с Version++ сделки (при желании — запись в таймлайн MemberAdded, deal-timeline-event.md).
  6. События. DealMembersChangedEvent → топик pin.crm.deal-members-changed.v1 (ContractVersion = 1, AggregateId = DealId) с дельтой Members = [{UserId, Role, ChangeType = Added}] и полным активным составом ActiveMembers после изменения — transactional outbox; контракт — deal-members-changed.md.
  7. Результат. Карточка добавленного участника → 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, ни интеграционного события.

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

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId/UserId = Guid.Empty или Role вне enum«Проверьте корректность запроса»no retry
CRM_PARTICIPANT_ROLE_FORBIDDEN422роль не добавляется через агентский эндпоинт (Executor/Responsible/InvestorPrimary/SellManager/PinManager)«Эта роль назначается другим способом»no retry
CRM_DEAL_NOT_FOUND404сделки нет или она недоступна актору (record-level скрывает существование)«Сделка не найдена»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9)«Сделка завершена и не редактируется»no retry
CRM_USER_NOT_FOUND404UserId не найден в реплике identity«Пользователь не найден»no retry
CRM_DUPLICATE_PARTICIPANT409UserId уже активный участник сделки (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; ФИО/телефон не пишутся); metrics crm_deal_member_added_total{role}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given нетерминальная доступная сделка и новый UserId; When POST с ролью CoAgent; Then 201, добавлена активная DealMemberEntity, Version++, опубликован deal-members-changed (ChangeType = Added, полный ActiveMembers).
  • Соинвестор. Given сделка с одним InvestorPrimary; When добавляют InvestorSecondary; Then 201 (произвольное число InvestorSecondary, singleton-инвариант не нарушен).
  • Дубль участника. Given UserId уже активный участник; When POST; Then 409 CRM_DUPLICATE_PARTICIPANT.
  • Недопустимая роль. Given Role = Executor/InvestorPrimary/SellManager/PinManager; When POST; Then 422 CRM_PARTICIPANT_ROLE_FORBIDDEN (эти роли — через смену роли/передачу ведения или системно).
  • Терминальная сделка. Given сделка Completed/Cancelled; When POST; Then 409 CRM_DEAL_ALREADY_TERMINAL.
  • Нет доступа. Given сделка, где актор не участник и не руководитель/ПИН; When POST; Then 404 CRM_DEAL_NOT_FOUND (существование не раскрывается).
  • Downstream (eventual). Given успешное добавление; When проходит < 1 мин; Then участник появляется в чате сделки (messenger) и получает уведомление (notifications).

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

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

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