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

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 areaagent
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/agent/engage-developer.md

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

ПроверкаЗначение
Actor typeUser — ответственный агент, руководитель агентства или Менеджер ПИН
Auth policy / Permissionscrm.deals.manage (агентская сторона); Менеджер ПИН — platform-scope
Scope (RBAC)организация (агентство): record-level по OriginAgencyCompanyId + участие исполнителя
Record-levelOriginAgencyCompanyId = actor.CompanyId; рядовой агент дополнительно — активный участник-исполнитель (ExecutorAgentUserId = actor.UserId); руководитель агентства — любая сделка своего OriginAgencyCompanyId; Менеджер ПИН — обход record-level
Tenant isolationСделка TenantId = DeveloperCompanyId (владелец объекта, ADR-0052; set-once). Операция агентской стороны идёт по явному предикату OriginAgencyCompanyId, а не по авто-tenant-фильтру
Auditupdate: Status, состав участников (+SellManager), IntentConfirmedAt, UpdatedByUserId, Version; бизнес-история — строки таймлайна StageChanged + MemberAdded
Доступ

Операция агентской стороны идёт по явному предикату OriginAgencyCompanyId, а не по авто-tenant-фильтру: TenantId сделки — это застройщик (владелец объекта), а не агентство. Менеджер ПИН обходит record-level; рядовой агент дополнительно должен быть активным участником-исполнителем сделки.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/deals/{dealId}/engage-developer
Success status200 (обновлённая карточка сделки в Application)
Idempotency headerне применяется — повтор на уже вовлечённую сделку отклоняется по статусу (CRM_DEAL_NOT_DRAFT), гонка ловится проверкой версии
Correlationсквозной trace через заголовок traceparent

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

TenantId в запросе не передаётся — он уже зафиксирован на сделке при создании (застройщик объекта).

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty; существующая сделка своего агентства, Status = Draft
PaymentMethodbodystring? (enum PaymentMethod)Условнообязателен, если на сделке ещё не задан PaymentMethod; одно из значений enum строкой; дом — purchase-program.md
InstallmentPresetIdbodyGuid?Условнообязателен при PaymentMethod = Installment (иначе CRM_DEAL_INSTALLMENT_PRESET_REQUIRED); пресет застройщика объекта, не архивирован
RowVersionbodyuint?Нетнаблюдаемая версия карточки для оптимистической блокировки
ActorUserIdidentity claimsGuidДаиз JWT
ActorAgencyCompanyIdidentity claimsGuidДаагентство актора — record-level предикат по OriginAgencyCompanyId сделки
IsAgencyHeadidentity claimsboolДафлаг «руководитель агентства» (доступ на все сделки своего OriginAgencyCompanyId)
IsPinManageridentity claimsboolДаplatform-scope Менеджера ПИН (обход record-level)

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

Проекция сделки после вовлечения застройщика; дом полей — deal.md, здесь только дельта. EscrowAccountNumber (Sensitive) и данные фиксации в карту не входят.

ПолеТипОбязательностьИсточникMaskingОписание
DealIdGuidДаDealEntity.IdИдентификатор сделки.
TitlestringДаDealEntity.TitleЗаголовок карточки.
Statusstring (enum DealStatus)ДаDealEntity.StatusВсегда Application после успешного перехода.
StepId / StepGroupGuid / string (enum PipelineStepGroup)ДаDealEntity (этап группы Application)Этап канбана застройщика.
IntentIdGuid?НетDealEntity.IntentIdНамерение по объекту.
IntentConfirmedAtDateTimeOffsetДаDealEntity.IntentConfirmedAtМомент подтверждения намерения инвестором (после перехода заполнен).
PaymentMethodstring (enum PaymentMethod)ДаDealEntity.PaymentMethodСпособ оплаты (после перехода обязателен).
PropertyObjectId / ProjectIdGuidДаDealEntityОбъект и проект.
PriceMoneyModelДаDealEntity.Price (owned)Стоимость объекта (snapshot).
MembersIReadOnlyList<DealMemberModel>Даактивные DealMemberEntityСостав с добавленным SellManager.
ChatStatusstring (enum DealChatStatus)ДаDealEntity.ChatStatusГотовность чата сделки.
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtМомент перехода.
RowVersionuintДаDealEntity.RowVersionТокен для следующей операции.

MoneyModel { Amount: decimal, Currency: string (char3, RUB) }; DealMemberModel { UserId: Guid, Role: string (enum ParticipantRole) } — дом ролей deal.md.

Алгоритм

Гейт INV-DEAL-11

Переход Draft → Application возможен, только когда одновременно выполнены три условия: намерение подтверждено инвестором, для пары «инвестор × застройщик» есть одобренная фиксация и задан способ оплаты (валидный пресет застройщика — при рассрочке). Подробности — в шаге 4.

  1. Контекст и доступ. Операцию выполняет ответственный агент / руководитель агентства / Менеджер ПИН (permission crm.deals.manage).

  2. Проверка входа и предусловий.

    • формат: 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 вовлечение застройщика неприменимо).
  3. Что читаем. Сделку (её IntentId, TenantId = DeveloperCompanyId, PrimaryInvestorUserId, PaymentMethod), связанное намерение по объекту (lead.md), одобренную фиксацию пары «инвестор × застройщик» (инвестор = PrimaryInvestorUserId, застройщик = TenantId), менеджера продаж застройщика для роли SellManager (из одобренной фиксации), при Installment — пресет застройщика.

  4. Гейт 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.
  5. Бизнес-правила и переходы. При выполненном гейте: 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).

  6. Что меняется. Сделка (Status, IntentConfirmedAt, FixationRequestId, при необходимости PaymentMethod), новый участник SellManager и строки таймлайна — в одном согласованном изменении.

  7. События. В одном согласованном изменении публикуются:

    • 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. С момента Application developer-facing консюмеры перестают фильтровать сделку (Status != Draft больше не отсекает), сделка видна застройщику.

  8. Результат. Карточка сделки в Application (состав с SellManager, IntentConfirmedAt, PaymentMethod, свежий RowVersion) → HTTP 200. При конкурентной правке (устаревший 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.

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

ТипДетали
Интеграционные событияDealStageChangedEventpin.crm.deal-stage-changed.v1 и DealMembersChangedEventpin.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_FAILED400DealId = Guid.Empty; PaymentMethod вне enum; Installment без InstallmentPresetId«Проверьте корректность запроса»no retry
CRM_DEAL_NOT_FOUND404сделки нет или она чужого агентства (record-level скрывает существование)«Сделка не найдена»no retry
CRM_DEAL_NOT_DRAFT409Status != Draft (застройщик уже вовлечён либо сделка терминальна)«Застройщик уже вовлечён»no retry
CRM_DEAL_INTENT_NOT_CONFIRMED409связанное намерение ConfirmedByInvestor = false (INV-DEAL-11)«Инвестор ещё не подтвердил объект»no retry (ждать подтверждения)
CRM_DEAL_FIXATION_REQUIRED409нет одобренной фиксации пары «инвестор × застройщик» (INV-DEAL-11)«Нужна одобренная фиксация клиента у застройщика»no retry (дождаться одобрения)
CRM_DEAL_PAYMENT_METHOD_REQUIRED422PaymentMethod не задан ни на сделке, ни в запросе (INV-DEAL-11/INV-DEAL-1)«Выберите способ оплаты»no retry
CRM_INSTALLMENT_PRESET_NOT_FOUND404пресет не найден«Программа рассрочки не найдена»no retry
CRM_INSTALLMENT_PRESET_FORBIDDEN409пресет чужого застройщика/архивирован«Программа недоступна»no retry
CRM_DEAL_CONFLICT409конкурентная правка (устаревший 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 не пишутся); metrics crm_deal_engage_developer_total{result}, latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given сделка Draft, намерение ConfirmedByInvestor = true, одобренная фиксация пары «инвестор × застройщик», задан PaymentMethod; When POST; Then 200, сделка Application, в составе SellManager, заполнены IntentConfirmedAt/FixationRequestId, опубликованы deal-stage-changed (Draft → Application) и deal-members-changed; сделка видна застройщику.
  • Способ оплаты в запросе. Given Draft без PaymentMethod, гейт по намерению и фиксации выполнен; When POST с paymentMethod; Then 200, PaymentMethod проставлен, сделка Application.
  • Намерение не подтверждено. Given намерение ConfirmedByInvestor = false; When POST; Then 409 CRM_DEAL_INTENT_NOT_CONFIRMED, сделка остаётся Draft.
  • Нет одобренной фиксации. Given намерение подтверждено, но одобренной фиксации пары нет; When POST; Then 409 CRM_DEAL_FIXATION_REQUIRED, сделка остаётся Draft.
  • Нет способа оплаты. Given гейт по намерению и фиксации выполнен, но PaymentMethod не задан и не передан; When POST; Then 422 CRM_DEAL_PAYMENT_METHOD_REQUIRED.
  • Уже вовлечён / не Draft. Given сделка в Application/Booking/Sale/Completed/Cancelled; When POST; Then 409 CRM_DEAL_NOT_DRAFT.
  • Чужая сделка. Given сделка другого агентства; When POST; Then 404 CRM_DEAL_NOT_FOUND (существование не раскрывается).
  • Гонка версий. Given параллельная правка карточки или устаревший RowVersion; When POST; Then 409 CRM_DEAL_CONFLICT; клиент перечитывает карточку.
  • Downstream (eventual). Given успешный переход; When проходит ≤ 5 мин; Then messenger добавляет SellManager в чат сделки, notifications уведомляет участников; сделка появляется на канбане застройщика.

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

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

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