POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application)
Агент переводит сделку из статуса Draft (сделка агента с инвестором, застройщик ещё не вовлечён) в
Application, когда выполнен гейт вовлечения застройщика: связанное намерение инвестора по объекту
подтверждено и для пары «инвестор × застройщик» есть одобренная фиксация. Переход добавляет в состав
SellManager, открывает сделку застройщику (она попадает на его канбан) и запускает developer-facing
публикации. Дом полей, статусы и инвариант гейта (INV-DEAL-11) — в deal.md.
Назначение
Дать агентской стороне довести Draft-сделку до полноценной заявки застройщика. До вовлечения сделка видна
только агенту/инвестору/Менеджеру ПИН и живёт без SellManager; с момента Application в неё добавляется
менеджер продаж застройщика, сделка становится видимой застройщику и участвует в его воронке. Гейт — точка,
где сходятся два условия из разных потоков: подтверждение инвестором намерения
(lead.md) и одобренная фиксация пары «инвестор ×
застройщик» (fixation-request.md).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/engage-developer.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — ответственный агент, руководитель агентства или Менеджер ПИН |
| Auth policy / Permissions | crm.deals.manage (агентская сторона); Менеджер ПИН — platform-scope |
| Scope (RBAC) | организация (агентство): record-level по OriginAgencyCompanyId + участие исполнителя |
| Record-level | OriginAgencyCompanyId = actor.CompanyId; рядовой агент дополнительно — активный участник-исполнитель (ExecutorAgentUserId = actor.UserId); руководитель агентства — любая сделка своего OriginAgencyCompanyId; Менеджер ПИН — обход record-level |
| Tenant isolation | Сделка TenantId = DeveloperCompanyId (владелец объекта, ADR-0052; set-once). Операция агентской стороны идёт по явному предикату OriginAgencyCompanyId, а не по авто-tenant-фильтру |
| Audit | update: Status, состав участников (+SellManager), IntentConfirmedAt, UpdatedByUserId, Version; бизнес-история — строки таймлайна StageChanged + MemberAdded |
Операция агентской стороны идёт по явному предикату OriginAgencyCompanyId, а не по авто-tenant-фильтру:
TenantId сделки — это застройщик (владелец объекта), а не агентство. Менеджер ПИН обходит record-level;
рядовой агент дополнительно должен быть активным участником-исполнителем сделки.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/deals/{dealId}/engage-developer |
| Success status | 200 (обновлённая карточка сделки в Application) |
| Idempotency header | не применяется — повтор на уже вовлечённую сделку отклоняется по статусу (CRM_DEAL_NOT_DRAFT), гонка ловится проверкой версии |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
TenantId в запросе не передаётся — он уже зафиксирован на сделке при создании (застройщик объекта).
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route | Guid | Да | != Guid.Empty; существующая сделка своего агентства, Status = Draft | — |
PaymentMethod | body | string? (enum PaymentMethod) | Условно | обязателен, если на сделке ещё не задан PaymentMethod; одно из значений enum строкой; дом — purchase-program.md | — |
InstallmentPresetId | body | Guid? | Условно | обязателен при PaymentMethod = Installment (иначе CRM_DEAL_INSTALLMENT_PRESET_REQUIRED); пресет застройщика объекта, не архивирован | — |
RowVersion | body | uint? | Нет | наблюдаемая версия карточки для оптимистической блокировки | — |
ActorUserId | identity claims | Guid | Да | из JWT | — |
ActorAgencyCompanyId | identity claims | Guid | Да | агентство актора — record-level предикат по OriginAgencyCompanyId сделки | — |
IsAgencyHead | identity claims | bool | Да | флаг «руководитель агентства» (доступ на все сделки своего OriginAgencyCompanyId) | — |
IsPinManager | identity claims | bool | Да | platform-scope Менеджера ПИН (обход record-level) | — |
Модель ответа DealCardModel
Проекция сделки после вовлечения застройщика; дом полей — deal.md, здесь только
дельта. EscrowAccountNumber (Sensitive) и данные фиксации в карту не входят.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | DealEntity.Id | — | Идентификатор сделки. |
Title | string | Да | DealEntity.Title | — | Заголовок карточки. |
Status | string (enum DealStatus) | Да | DealEntity.Status | — | Всегда Application после успешного перехода. |
StepId / StepGroup | Guid / string (enum PipelineStepGroup) | Да | DealEntity (этап группы Application) | — | Этап канбана застройщика. |
IntentId | Guid? | Нет | DealEntity.IntentId | — | Намерение по объекту. |
IntentConfirmedAt | DateTimeOffset | Да | DealEntity.IntentConfirmedAt | — | Момент подтверждения намерения инвестором (после перехода заполнен). |
PaymentMethod | string (enum PaymentMethod) | Да | DealEntity.PaymentMethod | — | Способ оплаты (после перехода обязателен). |
PropertyObjectId / ProjectId | Guid | Да | DealEntity | — | Объект и проект. |
Price | MoneyModel | Да | DealEntity.Price (owned) | — | Стоимость объекта (snapshot). |
Members | IReadOnlyList<DealMemberModel> | Да | активные DealMemberEntity | — | Состав с добавленным SellManager. |
ChatStatus | string (enum DealChatStatus) | Да | DealEntity.ChatStatus | — | Готовность чата сделки. |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | Момент перехода. |
RowVersion | uint | Да | DealEntity.RowVersion | — | Токен для следующей операции. |
MoneyModel { Amount: decimal, Currency: string (char3, RUB) }; DealMemberModel { UserId: Guid, Role: string (enum ParticipantRole) } — дом ролей deal.md.
Алгоритм
Переход Draft → Application возможен, только когда одновременно выполнены три условия: намерение
подтверждено инвестором, для пары «инвестор × застройщик» есть одобренная фиксация и задан способ оплаты
(валидный пресет застройщика — при рассрочке). Подробности — в шаге 4.
-
Контекст и доступ. Операцию выполняет ответственный агент / руководитель агентства / Менеджер ПИН (permission
crm.deals.manage). -
Проверка входа и предусловий.
- формат:
DealId!= Guid.Empty;PaymentMethod, если задан, — из enum; приPaymentMethod = Installment— заданInstallmentPresetId(иначеCRM_DEAL_INSTALLMENT_PRESET_REQUIRED) — иначе ошибка валидации; - сделка существует и принадлежит агентству актора (record-level предикат) — иначе
CRM_DEAL_NOT_FOUND(404, существование чужих сделок не раскрывается); - сделка в статусе
Draft— иначеCRM_DEAL_NOT_DRAFT(409; изApplication/Booking/Sale/Completed/Cancelledвовлечение застройщика неприменимо).
- формат:
-
Что читаем. Сделку (её
IntentId,TenantId = DeveloperCompanyId,PrimaryInvestorUserId,PaymentMethod), связанное намерение по объекту (lead.md), одобренную фиксацию пары «инвестор × застройщик» (инвестор =PrimaryInvestorUserId, застройщик =TenantId), менеджера продаж застройщика для ролиSellManager(из одобренной фиксации), приInstallment— пресет застройщика. -
Гейт
Draft → Application(INV-DEAL-11). Переход разрешён, только когда одновременно:- связанное намерение подтверждено инвестором (
ConfirmedByInvestor = true) — иначеCRM_DEAL_INTENT_NOT_CONFIRMED; - для пары «инвестор × застройщик» есть одобренная фиксация — иначе
CRM_DEAL_FIXATION_REQUIRED; - задан
PaymentMethod— на сделке или переданный в запросе; приInstallment— валидныйInstallmentPresetIdзастройщика объекта (иначеCRM_INSTALLMENT_PRESET_NOT_FOUND/CRM_INSTALLMENT_PRESET_FORBIDDEN); отсутствие способа оплаты —CRM_DEAL_PAYMENT_METHOD_REQUIRED.
- связанное намерение подтверждено инвестором (
-
Бизнес-правила и переходы. При выполненном гейте:
Status: Draft → Application; в состав добавитьSellManager(менеджер продаж застройщика; INV-DEAL-2/INV-DEAL-3 — не более одного активного);IntentConfirmedAt = intent.ConfirmedAt;FixationRequestId= идентификатор одобренной фиксации (связь фиксация→сделки 1:N — значение не уникально); еслиPaymentMethodпришёл в запросе — проставить его (иInstallmentPresetIdприInstallment);UpdatedByUserId = actor,UpdatedAt = now. В таймлайн — строкиStageChanged(Draft → Application) иMemberAdded(SellManager) (deal-timeline-event.md, append-only). -
Что меняется. Сделка (
Status,IntentConfirmedAt,FixationRequestId, при необходимостиPaymentMethod), новый участникSellManagerи строки таймлайна — в одном согласованном изменении. -
События. В одном согласованном изменении публикуются:
DealStageChangedEvent→ топикpin.crm.deal-stage-changed.v1(ContractVersion = 1,AggregateId = DealId), payload{ DealId, TenantId, FromStepId?, ToStepId, FromStepGroup?, ToStepGroup = Application, FromStatus = Draft, ToStatus = Application, ChangedByUserId = ActorUserId, ChangedAt = UpdatedAt }— контракт deal-stage-changed.md;DealMembersChangedEvent→ топикpin.crm.deal-members-changed.v1(ContractVersion = 1), полный активный состав с добавленнымSellManager— контракт deal-members-changed.md;
всё через transactional outbox. С момента
Applicationdeveloper-facing консюмеры перестают фильтровать сделку (Status != Draftбольше не отсекает), сделка видна застройщику. -
Результат. Карточка сделки в
Application(состав сSellManager,IntentConfirmedAt,PaymentMethod, свежийRowVersion) → HTTP200. При конкурентной правке (устаревшийRowVersion) —CRM_DEAL_CONFLICT(409); клиент перечитывает карточку.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — ответственный агент / руководитель агентства / Менеджер ПИН с crm.deals.manage.
Сделка существует, принадлежит агентству актора и находится в Draft. Связанное намерение подтверждено
инвестором (ConfirmedByInvestor = true). Для пары «инвестор × застройщик» есть одобренная фиксация. Задан
PaymentMethod (на сделке или в запросе); при Installment — валидный пресет застройщика.
Постусловие (успех). Deal.Status = Application; в составе появился SellManager; заполнены
IntentConfirmedAt и FixationRequestId; PaymentMethod задан; UpdatedByUserId = actor, UpdatedAt = now, RowVersion инкрементирован. В таймлайне — строки StageChanged (Draft → Application) и MemberAdded.
Опубликованы DealStageChangedEvent (pin.crm.deal-stage-changed.v1) и DealMembersChangedEvent
(pin.crm.deal-members-changed.v1), key DealId. Окно eventual-consistency: сделка становится видимой
застройщику сразу в CRM, но синхронизация участников чата (messenger), уведомление застройщику/участникам
(notifications) и developer-facing проекции появляются с лагом (SLA консюмеров < 5 мин); порядок событий
per-deal гарантирован ключом DealId.
Постусловие (отказ). Ни смены статуса, ни нового участника, ни строк таймлайна, ни событий — сделка
остаётся в Draft.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | DealStageChangedEvent → pin.crm.deal-stage-changed.v1 и DealMembersChangedEvent → pin.crm.deal-members-changed.v1; всё в outbox |
| Внешние вызовы | none (намерение, фиксация и пресет — локальные таблицы crm) |
| Проекции / поиск | сделка перестаёт скрываться developer-facing фильтром Status != Draft; проекция сделки (DealSearchDocument) обновляется по Status/составу |
| Уведомления | опосредованно: консюмер deal-stage-changed/deal-members-changed в notifications (уведомление участникам, в т.ч. добавленному SellManager); messenger синхронизирует участников чата |
Ошибки и failure modes
CRM_DEAL_NOT_FOUND маскирует существование чужой сделки (404, а не 403). Повторять запрос имеет смысл
только при CRM_DEAL_CONFLICT (устаревший RowVersion) — остальные коды требуют сначала выполнить
бизнес-условие (подтверждение намерения, одобрение фиксации, выбор способа оплаты).
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId = Guid.Empty; PaymentMethod вне enum; Installment без InstallmentPresetId | «Проверьте корректность запроса» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки нет или она чужого агентства (record-level скрывает существование) | «Сделка не найдена» | no retry |
CRM_DEAL_NOT_DRAFT | 409 | Status != Draft (застройщик уже вовлечён либо сделка терминальна) | «Застройщик уже вовлечён» | no retry |
CRM_DEAL_INTENT_NOT_CONFIRMED | 409 | связанное намерение ConfirmedByInvestor = false (INV-DEAL-11) | «Инвестор ещё не подтвердил объект» | no retry (ждать подтверждения) |
CRM_DEAL_FIXATION_REQUIRED | 409 | нет одобренной фиксации пары «инвестор × застройщик» (INV-DEAL-11) | «Нужна одобренная фиксация клиента у застройщика» | no retry (дождаться одобрения) |
CRM_DEAL_PAYMENT_METHOD_REQUIRED | 422 | PaymentMethod не задан ни на сделке, ни в запросе (INV-DEAL-11/INV-DEAL-1) | «Выберите способ оплаты» | no retry |
CRM_INSTALLMENT_PRESET_NOT_FOUND | 404 | пресет не найден | «Программа рассрочки не найдена» | no retry |
CRM_INSTALLMENT_PRESET_FORBIDDEN | 409 | пресет чужого застройщика/архивирован | «Программа недоступна» | no retry |
CRM_DEAL_CONFLICT | 409 | конкурентная правка (устаревший RowVersion) | «Сделка изменена, обновите данные» | retry (перечитать карточку) |
Совместимость и наблюдаемость
- Контракт: route +
PaymentMethod/InstallmentPresetId/RowVersion(body) →DealCardModel. Новые optional-поля — additive; смена маршрута/семантики гейта — breaking (migration + rollout). Новые значенияPaymentMethod/DealStatus/ParticipantRole— additive. - Logs:
deal engage-developer requested/engaged/rejected/conflict(DealId,TenantId,PrimaryInvestorUserId,IntentId,FixationRequestId,ActorUserId; ФИО/PII не пишутся); metricscrm_deal_engage_developer_total{result}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given сделка
Draft, намерениеConfirmedByInvestor = true, одобренная фиксация пары «инвестор × застройщик», заданPaymentMethod; WhenPOST; Then200, сделкаApplication, в составеSellManager, заполненыIntentConfirmedAt/FixationRequestId, опубликованыdeal-stage-changed(Draft → Application) иdeal-members-changed; сделка видна застройщику. - Способ оплаты в запросе. Given
DraftбезPaymentMethod, гейт по намерению и фиксации выполнен; WhenPOSTсpaymentMethod; Then200,PaymentMethodпроставлен, сделкаApplication. - Намерение не подтверждено. Given намерение
ConfirmedByInvestor = false; WhenPOST; Then409 CRM_DEAL_INTENT_NOT_CONFIRMED, сделка остаётсяDraft. - Нет одобренной фиксации. Given намерение подтверждено, но одобренной фиксации пары нет; When
POST; Then409 CRM_DEAL_FIXATION_REQUIRED, сделка остаётсяDraft. - Нет способа оплаты. Given гейт по намерению и фиксации выполнен, но
PaymentMethodне задан и не передан; WhenPOST; Then422 CRM_DEAL_PAYMENT_METHOD_REQUIRED. - Уже вовлечён / не Draft. Given сделка в
Application/Booking/Sale/Completed/Cancelled; WhenPOST; Then409 CRM_DEAL_NOT_DRAFT. - Чужая сделка. Given сделка другого агентства; When
POST; Then404 CRM_DEAL_NOT_FOUND(существование не раскрывается). - Гонка версий. Given параллельная правка карточки или устаревший
RowVersion; WhenPOST; Then409 CRM_DEAL_CONFLICT; клиент перечитывает карточку. - Downstream (eventual). Given успешный переход; When проходит ≤ 5 мин; Then
messengerдобавляетSellManagerв чат сделки,notificationsуведомляет участников; сделка появляется на канбане застройщика.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/agent/crm/deals — Создать сделку из лида по объекту
- Use Cases (3): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика)
- События (2): Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности (дом полей,
DealStatus.Draft, INV-DEAL-1/2/11,ParticipantRole): deal.md; намерение по объекту (ConfirmedByInvestor,ConfirmedAt): lead.md; авторизация работы с клиентом: fixation-request.md; способ оплаты/пресет: purchase-program.md; таймлайнStageChanged/MemberAdded: deal-timeline-event.md. - События: deal-stage-changed.md (
Draft → Application), deal-members-changed.md (добавленSellManager). - Создание сделки (откуда берётся
Draft): create-deal.md; подтверждение намерения инвестором: confirm-booking-intent.md; карта области — README контекста. - ADR: 0052 tenancy, 0055 мини-мессенджер, 0071 company-ownership и участники.