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

POST /api/agent/crm/deals — Создать сделку из лида по объекту

Агент нажимает «Создать сделку» в карточке лида (/crm/leads/[id]) после выбора объекта. Сделка попадает в один из двух статусов по гейту вовлечения застройщика: если для пары «инвестор × застройщик» есть одобренная фиксация (fixation-request.md) и объект — подтверждённое инвестором намерение (lead.md) — сразу Application (застройщик вовлечён); иначе — Draft (сделка агента с инвестором, застройщик ещё не вовлечён; довести до Application позднее — engage-developer.md).

Назначение

Из активного лида создать классическую сделку «инвестор + агент + застройщик + объект»: выбрать объект недвижимости (владелец/цена — из реплики chessboard), связать её с намерением инвестора по этому объекту, собрать начальный состав участников и — по выполнению гейта — либо сразу вовлечь застройщика (Application, SellManager в составе, попадание на канбан застройщика), либо оставить сделку в Draft (только агент + инвестор, невидима застройщику). В любом случае поднимается событие для чата сделки (messenger, ADR-0055).

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/agent/create-deal.md

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

ПроверкаЗначение
Actor typeUser (Агент — владелец лида; Менеджер ПИН — platform-scope)
Auth policy / Permissionscrm.deals.manage
Scope (RBAC)организация (агентство актора); сделка создаётся в tenant застройщика (кросс-tenant по ADR-0052)
Record-levelвладелец лида (актор — активный участник-исполнитель, lead.ExecutorAgentUserId = actor) либо Менеджер ПИН (сквозной доступ)
Tenant isolationTenantId сделки = DeveloperCompanyId владельца объекта (вычисляется); доступ к лиду — по record-level его AgencyCompanyId
Auditcreate (CreatedByUserId = actor)
Доступ

Создать сделку может только владелец лида (агент, lead.ExecutorAgentUserId = actor) либо Менеджер ПИН (platform-scope, сквозной доступ). Сделка создаётся кросс-tenant — в tenant застройщика объекта (TenantId = DeveloperCompanyId), а не в tenant агентства актора (ADR-0052).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/deals
Success status201 (создано)
Idempotency headerне применяется (у лида допустимо несколько сделок — естественного дедуп-ключа нет; двойной клик гасится на клиенте)
Correlationсквозной trace через заголовок traceparent

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

Важно

TenantId в запрос не передаётся: он вычисляется из владельца PropertyObjectId.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаагент/Менеджер ПИН; record-level по лиду
LeadIdbodyGuidДа!= Guid.Empty; существует, Status = Active, record-level актора
PropertyObjectIdbodyGuidДа!= Guid.Empty; существующий объект недвижимости chessboard, не Продан
PaymentMethodbodystring (enum PaymentMethod)Условноодно из значений enum строкой; обязателен, только если сделка создаётся сразу в Application (гейт выполнен): при выполненном гейте отсутствие → CRM_DEAL_PAYMENT_METHOD_REQUIRED (INV-DEAL-1); при создании в Draft — необязателен (задаётся позже, на вовлечении застройщика); дом — purchase-program.md
InstallmentPresetIdbodyGuid?Условнообязателен при PaymentMethod = Installment → иначе CRM_DEAL_INSTALLMENT_PRESET_REQUIRED; пресет застройщика объекта, не архивирован
Titlebodystring?Нет1..256; null → генерируется из объекта («Объект №…, ЖК …»)

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

Проекция созданной сделки (дом полей — deal.md; ниже — только отдаваемая дельта).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealEntity.IdИдентификатор сделки.
TenantIdGuidДаDealEntity.TenantIdDeveloperCompanyId застройщика объекта.
LeadIdGuidДаDealEntity.LeadIdЛид-основание.
ClientIdGuidДаDealEntity.ClientIdКарточка клиента.
OriginAgentUserId / ExecutorAgentUserId / OriginAgencyCompanyId / PrimaryInvestorUserIdGuidДаиз лида (Origin — иммутабельный снимок; Executor = создатель)Стороны сделки (при создании ExecutorAgentUserId = OriginAgentUserId).
PropertyObjectId / ProjectIdGuidДаDealEntity (snapshot chessboard)Объект и проект.
TitlestringДаDealEntity.TitleЗаголовок карточки.
Statusstring (enum DealStatus)ДаDealEntity.StatusApplication (гейт выполнен) либо Draft (гейт не выполнен).
IntentIdGuid?НетDealEntity.IntentIdНамерение инвестора по объекту, к которому привязана сделка (lead.md).
IntentConfirmedAtDateTimeOffset?НетDealEntity.IntentConfirmedAtМомент подтверждения намерения инвестором; null при создании в Draft с неподтверждённым намерением.
StepId / StepGroupGuid / string (enum PipelineStepGroup)Даначальный этап группы Application (pipeline-step.md)Стартовая колонка канбана.
PaymentMethodstring? (enum PaymentMethod)НетDealEntity.PaymentMethodСпособ оплаты; null допустим у Draft.
InstallmentPresetIdGuid?НетDealEntity.InstallmentPresetIdПресет рассрочки (при Installment).
PriceMoneyModelДаDealEntity.Price (owned Money)Цена объекта недвижимости на момент создания.
MembersIReadOnlyList<DealMemberModel>Даактивные DealMemberEntityНачальный состав (модель ниже); SellManager присутствует только при Application.
ChatStatusstring (enum DealChatStatus)ДаDealEntity.ChatStatusPending (чат создаст messenger).
CreatedAtDateTimeOffsetДаDealEntity.CreatedAtМомент создания.

MoneyModel { Amount: decimal, Currency: string (char3, RUB) }; DealMemberModel { UserId: Guid, Role: string (enum ParticipantRole) } — дом ролей deal.md. Списки сделок агента — отдельные query-методы; здесь ответ скалярный (одна карточка).

Алгоритм

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

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

    • формат: LeadId/PropertyObjectId != Guid.Empty; PaymentMethod, если задан, — из enum; при PaymentMethod = Installment — задан InstallmentPresetId (иначе CRM_DEAL_INSTALLMENT_PRESET_REQUIRED); Title ≤ 256 — иначе ошибка валидации;
    • лид существует — иначе CRM_LEAD_NOT_FOUND;
    • лид доступен актору (владелец либо Менеджер ПИН) — иначе CRM_LEAD_FORBIDDEN;
    • лид активен (Status = Active, INV-DEAL-1 / INV-LEAD-2) — иначе CRM_LEAD_NOT_ACTIVE;
    • объект есть в реплике chessboard — иначе CRM_PROPERTY_OBJECT_NOT_FOUND; источник недоступен → CHESSBOARD_UNAVAILABLE; из ответа берутся DeveloperCompanyId (→ TenantId сделки), ProjectId, BuildingId?, Price, статус; статус ПроданCRM_PROPERTY_OBJECT_NOT_AVAILABLE;
    • при Installment пресет существует (иначе CRM_INSTALLMENT_PRESET_NOT_FOUND), принадлежит застройщику объекта и не архивирован (иначе CRM_INSTALLMENT_PRESET_FORBIDDEN).
  3. Что читаем. Лид (источник сторон: ClientId, агентство, инвестор, агент), карточку клиента (ФИО — snapshot для проекции сделки), активное намерение инвестора по объекту (LeadBookingIntentEntity для пары (LeadId, PropertyObjectId), lead.md), одобренную фиксацию пары «инвестор × застройщик» (DeveloperCompanyId объекта), начальный этап группы Application у застройщика — если живого этапа нет, CRM_DEAL_PIPELINE_NOT_CONFIGURED.

  4. Вычисление гейта вовлечения застройщика. Гейт выполнен, когда одновременно: объект — подтверждённое инвестором намерение (ConfirmedByInvestor = true) И для пары «инвестор × застройщик» есть одобренная фиксация. Если гейт выполнен — целевой статус Application, и тогда PaymentMethod обязателен (иначе CRM_DEAL_PAYMENT_METHOD_REQUIRED, INV-DEAL-1); если гейт не выполнен — целевой статус Draft (PaymentMethod необязателен).

  5. Бизнес-правила и переходы. Title = переданный ?? gen(объект). Привязать сделку к намерению по объекту: если активного намерения ещё нет — добавить его как AgentProposed (ConfirmedByInvestor = false, Flow B), иначе использовать существующее; IntentId = его идентификатор, IntentConfirmedAt = его ConfirmedAt. Создать сделку с ценой из chessboard, начальным этапом группы Application, ChatStatus = Pending, EscrowStatus = None:

    • гейт выполнен → Status = Application, состав InvestorPrimary + Executor + SellManager + авто-PinManager (INV-DEAL-2);
    • гейт не выполнен → Status = Draft, состав InvestorPrimary + Executor + авто-PinManager без SellManager (INV-DEAL-2); застройщик вовлекается позже (engage-developer.md).

    В ленту лида добавить событие LeadEventType.DealCreated (payload LeadDealRefPayload { DealId, PropertyObjectId, DeveloperCompanyId }, INV-LEAD-2) и обновить LastEventAt; в таймлайн сделки — запись DealCreated (append-only, deal-timeline-event.md).

  6. Что меняется. Появляются сделка (+участники +таймлайн), при Flow B — новое намерение AgentProposed, событие ленты лида — в одном согласованном изменении.

  7. События. DealCreatedEvent → топик pin.crm.deal-created.v1 (ContractVersion = 1, AggregateId = DealId, AggregateType = "Deal"), payload {DealId, TenantId, AgencyCompanyId, OriginAgencyCompanyId, OriginAgentUserId, ExecutorAgentUserId, InvestorUserId, DeveloperManagerUserId (= id SellManagerприApplication, иначе null), PropertyObjectId, ProjectId, StepId, Title, Status (Application|Draft), PaymentMethod (может отсутствовать при Draft), Members = [{UserId, Role}], CreatedAt} — transactional outbox; контракт — deal-created.md. Для Draft developer-facing консюмеры событие игнорируют (Status != Draft); messenger/notifications реагируют.

  8. Результат. Карточка созданной сделки (+ активные участники, цена, Status) → HTTP 201.

Диаграмма

Предусловия и постусловия

Предусловия. Актор — владелец лида / Менеджер ПИН с crm.deals.manage. Лид активен (Status = Active), доступен актору. Объект есть в реплике chessboard и не Продан. При PaymentMethod = Installment — задан валидный пресет застройщика объекта (не архивирован). У застройщика настроен живой этап группы Application.

Ожидания от chessboard

Из ответа берутся DeveloperCompanyId (→ TenantId сделки), ProjectId, BuildingId?, Price, статус — snapshot на момент операции. При недоступности источника команда завершается отказом (CHESSBOARD_UNAVAILABLE), а не работает по устаревшим данным.

Постусловие (успех). Создана DealEntity, привязанная к намерению инвестора по объекту (IntentId), стартовый этап группы Application, TenantId = DeveloperCompanyId, snapshot цены/проекта/здания, ChatStatus = Pending, EscrowStatus = None. Целевой статус по гейту:

  • гейт выполнен (подтверждённое намерение + одобренная фиксация): Status = Application, состав InvestorPrimary + Executor + SellManager + авто-PinManager (INV-DEAL-2), IntentConfirmedAt заполнен, сделка видна застройщику и попадает на его канбан;
  • гейт не выполнен: Status = Draft, состав InvestorPrimary + Executor + авто-PinManager без SellManager, PaymentMethod может быть null, сделка невидима застройщику (Status != Draft); при Flow B добавлено намерение AgentProposed (ConfirmedByInvestor = false).

В ленте лида — событие DealCreated, LastEventAt обновлён; в таймлайне сделки — DealCreated. Опубликован DealCreatedEvent (pin.crm.deal-created.v1, key DealId) — потребители messenger (создаёт чат сделки; ChatStatus: Pending → Ready асинхронно) и notifications реагируют и на Draft; developer-facing консюмеры — только на Application.

Eventual consistency

До долёта события карточка сделки показывает ChatStatus = Pending — чата ещё нет.

Постусловие (отказ). Ни сделки (+участников/таймлайна), ни события ленты лида, ни интеграционного события.

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

ТипДетали
Интеграционные событияDealCreatedEventpin.crm.deal-created.v1 (messenger создаёт чат), outbox
Внешние вызовыреплика chessboard: объект недвижимости (владелец/цена/проект/статус, timeout 2 c, retry 1)
Проекции / поискпроекция сделок (DealSearchDocument: Title + ФИО клиента snapshot) обновляется проектором; leads.LastEventAt
Уведомленияопосредованно: консюмер deal-created в notifications (уведомление участникам)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат LeadId/PropertyObjectId/Title«Проверьте корректность полей формы»no retry
CRM_DEAL_PAYMENT_METHOD_REQUIRED422гейт выполнен (сделка идёт в Application), но PaymentMethod не задан (INV-DEAL-1)«Выберите способ оплаты»no retry
CRM_DEAL_INSTALLMENT_PRESET_REQUIRED422Installment без InstallmentPresetId«Выберите программу рассрочки»no retry
CRM_LEAD_NOT_FOUND404лид не найден«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403лид чужого агента/tenant«Нет доступа к лиду»no retry
CRM_LEAD_NOT_ACTIVE409lead.Status != Active (INV-DEAL-1)«Лид неактивен»no retry
CRM_PROPERTY_OBJECT_NOT_FOUND404объект не найден в chessboard«Объект не найден»no retry
CRM_PROPERTY_OBJECT_NOT_AVAILABLE409статус объекта Продан«Объект уже продан»no retry
CRM_INSTALLMENT_PRESET_NOT_FOUND404пресет не найден«Программа рассрочки не найдена»no retry
CRM_INSTALLMENT_PRESET_FORBIDDEN409пресет чужого застройщика/архивирован«Программа недоступна»no retry
CRM_DEAL_PIPELINE_NOT_CONFIGURED409нет живого этапа группы Application у застройщика«Воронка застройщика не настроена»no retry
CHESSBOARD_UNAVAILABLE503chessboard недоступен«Сервис временно недоступен»retry

Совместимость и наблюдаемость

  • Новые optional поля запроса/ответа — additive, без breaking; удаление/переименование/смена типа — breaking (migration + rollout). Unknown enum PaymentMethodValidationError.
  • Logs: deal created (DealId, LeadId, TenantId, PropertyObjectId, Status; ФИО/PII не пишутся); metrics crm_deals_created_total{payment_method,status} (status = draft|application), latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy сразу в Application (гейт выполнен). Given активный лид, свободный объект, подтверждённое инвестором намерение по объекту, одобренная фиксация пары «инвестор × застройщик» и заданный способ оплаты; When POST; Then 201, сделка Application, участники InvestorPrimary/Executor/SellManager/PinManager, IntentId/IntentConfirmedAt заполнены, ChatStatus = Pending, опубликован deal-created (Status = Application).
  • Создание в Draft (гейт не выполнен). Given активный лид и свободный объект, но нет подтверждённого намерения и/или одобренной фиксации; When POST (PaymentMethod можно не передавать); Then 201, сделка Draft, участники InvestorPrimary/Executor/PinManager (без SellManager), при Flow B создано намерение AgentProposed, сделка невидима застройщику, опубликован deal-created (Status = Draft).
  • Без способа оплаты при выполненном гейте (INV-DEAL-1). Given гейт выполнен, но PaymentMethod не задан; When POST; Then 422 CRM_DEAL_PAYMENT_METHOD_REQUIRED.
  • Installment без пресета. Given PaymentMethod = Installment без InstallmentPresetId; When POST; Then 422 CRM_DEAL_INSTALLMENT_PRESET_REQUIRED.
  • Проданный объект. Given статус объекта Продан; When POST; Then 409 CRM_PROPERTY_OBJECT_NOT_AVAILABLE.
  • Источник недоступен. Given chessboard не отвечает; When POST; Then 503 CHESSBOARD_UNAVAILABLE (retry).
  • Воронка не настроена. Given у застройщика нет живого этапа группы Application; When POST; Then 409 CRM_DEAL_PIPELINE_NOT_CONFIGURED.
  • Неактивный лид. Given lead.Status != Active; When POST; Then 409 CRM_LEAD_NOT_ACTIVE.

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

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

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