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 area | agent |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/create-deal.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент — владелец лида; Менеджер ПИН — platform-scope) |
| Auth policy / Permissions | crm.deals.manage |
| Scope (RBAC) | организация (агентство актора); сделка создаётся в tenant застройщика (кросс-tenant по ADR-0052) |
| Record-level | владелец лида (актор — активный участник-исполнитель, lead.ExecutorAgentUserId = actor) либо Менеджер ПИН (сквозной доступ) |
| Tenant isolation | TenantId сделки = DeveloperCompanyId владельца объекта (вычисляется); доступ к лиду — по record-level его AgencyCompanyId |
| Audit | create (CreatedByUserId = actor) |
Создать сделку может только владелец лида (агент, lead.ExecutorAgentUserId = actor) либо Менеджер ПИН
(platform-scope, сквозной доступ). Сделка создаётся кросс-tenant — в tenant застройщика объекта
(TenantId = DeveloperCompanyId), а не в tenant агентства актора (ADR-0052).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/deals |
| Success status | 201 (создано) |
| Idempotency header | не применяется (у лида допустимо несколько сделок — естественного дедуп-ключа нет; двойной клик гасится на клиенте) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
TenantId в запрос не передаётся: он вычисляется из владельца PropertyObjectId.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | агент/Менеджер ПИН; record-level по лиду | — |
LeadId | body | Guid | Да | != Guid.Empty; существует, Status = Active, record-level актора | — |
PropertyObjectId | body | Guid | Да | != Guid.Empty; существующий объект недвижимости chessboard, не Продан | — |
PaymentMethod | body | string (enum PaymentMethod) | Условно | одно из значений enum строкой; обязателен, только если сделка создаётся сразу в Application (гейт выполнен): при выполненном гейте отсутствие → CRM_DEAL_PAYMENT_METHOD_REQUIRED (INV-DEAL-1); при создании в Draft — необязателен (задаётся позже, на вовлечении застройщика); дом — purchase-program.md | — |
InstallmentPresetId | body | Guid? | Условно | обязателен при PaymentMethod = Installment → иначе CRM_DEAL_INSTALLMENT_PRESET_REQUIRED; пресет застройщика объекта, не архивирован | — |
Title | body | string? | Нет | 1..256; null → генерируется из объекта («Объект №…, ЖК …») | — |
Модель ответа DealCardModel
Проекция созданной сделки (дом полей — deal.md; ниже — только отдаваемая дельта).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id | — | Идентификатор сделки. |
TenantId | Guid | Да | DealEntity.TenantId | — | DeveloperCompanyId застройщика объекта. |
LeadId | Guid | Да | DealEntity.LeadId | — | Лид-основание. |
ClientId | Guid | Да | DealEntity.ClientId | — | Карточка клиента. |
OriginAgentUserId / ExecutorAgentUserId / OriginAgencyCompanyId / PrimaryInvestorUserId | Guid | Да | из лида (Origin — иммутабельный снимок; Executor = создатель) | — | Стороны сделки (при создании ExecutorAgentUserId = OriginAgentUserId). |
PropertyObjectId / ProjectId | Guid | Да | DealEntity (snapshot chessboard) | — | Объект и проект. |
Title | string | Да | DealEntity.Title | — | Заголовок карточки. |
Status | string (enum DealStatus) | Да | DealEntity.Status | — | Application (гейт выполнен) либо Draft (гейт не выполнен). |
IntentId | Guid? | Нет | DealEntity.IntentId | — | Намерение инвестора по объекту, к которому привязана сделка (lead.md). |
IntentConfirmedAt | DateTimeOffset? | Нет | DealEntity.IntentConfirmedAt | — | Момент подтверждения намерения инвестором; null при создании в Draft с неподтверждённым намерением. |
StepId / StepGroup | Guid / string (enum PipelineStepGroup) | Да | начальный этап группы Application (pipeline-step.md) | — | Стартовая колонка канбана. |
PaymentMethod | string? (enum PaymentMethod) | Нет | DealEntity.PaymentMethod | — | Способ оплаты; null допустим у Draft. |
InstallmentPresetId | Guid? | Нет | DealEntity.InstallmentPresetId | — | Пресет рассрочки (при Installment). |
Price | MoneyModel | Да | DealEntity.Price (owned Money) | — | Цена объекта недвижимости на момент создания. |
Members | IReadOnlyList<DealMemberModel> | Да | активные DealMemberEntity | — | Начальный состав (модель ниже); SellManager присутствует только при Application. |
ChatStatus | string (enum DealChatStatus) | Да | DealEntity.ChatStatus | — | Pending (чат создаст messenger). |
CreatedAt | DateTimeOffset | Да | DealEntity.CreatedAt | — | Момент создания. |
MoneyModel { Amount: decimal, Currency: string (char3, RUB) }; DealMemberModel { UserId: Guid, Role: string (enum ParticipantRole) } — дом ролей deal.md. Списки сделок агента — отдельные
query-методы; здесь ответ скалярный (одна карточка).
Алгоритм
-
Контекст и доступ. Операцию выполняет агент — владелец лида (permission
crm.deals.manage) либо Менеджер ПИН (platform-scope). -
Проверка входа и предусловий.
- формат:
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).
- формат:
-
Что читаем. Лид (источник сторон:
ClientId, агентство, инвестор, агент), карточку клиента (ФИО — snapshot для проекции сделки), активное намерение инвестора по объекту (LeadBookingIntentEntityдля пары (LeadId,PropertyObjectId), lead.md), одобренную фиксацию пары «инвестор × застройщик» (DeveloperCompanyIdобъекта), начальный этап группыApplicationу застройщика — если живого этапа нет,CRM_DEAL_PIPELINE_NOT_CONFIGURED. -
Вычисление гейта вовлечения застройщика. Гейт выполнен, когда одновременно: объект — подтверждённое инвестором намерение (
ConfirmedByInvestor = true) И для пары «инвестор × застройщик» есть одобренная фиксация. Если гейт выполнен — целевой статусApplication, и тогдаPaymentMethodобязателен (иначеCRM_DEAL_PAYMENT_METHOD_REQUIRED, INV-DEAL-1); если гейт не выполнен — целевой статусDraft(PaymentMethodнеобязателен). -
Бизнес-правила и переходы.
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(payloadLeadDealRefPayload { DealId, PropertyObjectId, DeveloperCompanyId }, INV-LEAD-2) и обновитьLastEventAt; в таймлайн сделки — записьDealCreated(append-only, deal-timeline-event.md). - гейт выполнен →
-
Что меняется. Появляются сделка (+участники +таймлайн), при Flow B — новое намерение
AgentProposed, событие ленты лида — в одном согласованном изменении. -
События.
DealCreatedEvent→ топикpin.crm.deal-created.v1(ContractVersion = 1,AggregateId = DealId,AggregateType = "Deal"), payload{DealId, TenantId, AgencyCompanyId, OriginAgencyCompanyId, OriginAgentUserId, ExecutorAgentUserId, InvestorUserId, DeveloperManagerUserId (= idSellManagerприApplication, иначеnull), PropertyObjectId, ProjectId, StepId, Title, Status (Application|Draft), PaymentMethod (может отсутствовать приDraft), Members = [{UserId, Role}], CreatedAt}— transactional outbox; контракт — deal-created.md. ДляDraftdeveloper-facing консюмеры событие игнорируют (Status != Draft);messenger/notificationsреагируют. -
Результат. Карточка созданной сделки (+ активные участники, цена,
Status) → HTTP201.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — владелец лида / Менеджер ПИН с crm.deals.manage. Лид активен (Status = Active),
доступен актору. Объект есть в реплике chessboard и не Продан. При PaymentMethod = Installment — задан
валидный пресет застройщика объекта (не архивирован). У застройщика настроен живой этап группы Application.
Из ответа берутся 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.
До долёта события карточка сделки показывает ChatStatus = Pending — чата ещё нет.
Постусловие (отказ). Ни сделки (+участников/таймлайна), ни события ленты лида, ни интеграционного события.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | DealCreatedEvent → pin.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_FAILED | 400 | формат LeadId/PropertyObjectId/Title | «Проверьте корректность полей формы» | no retry |
CRM_DEAL_PAYMENT_METHOD_REQUIRED | 422 | гейт выполнен (сделка идёт в Application), но PaymentMethod не задан (INV-DEAL-1) | «Выберите способ оплаты» | no retry |
CRM_DEAL_INSTALLMENT_PRESET_REQUIRED | 422 | Installment без InstallmentPresetId | «Выберите программу рассрочки» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден | «Лид не найден» | no retry |
CRM_LEAD_FORBIDDEN | 403 | лид чужого агента/tenant | «Нет доступа к лиду» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | lead.Status != Active (INV-DEAL-1) | «Лид неактивен» | no retry |
CRM_PROPERTY_OBJECT_NOT_FOUND | 404 | объект не найден в chessboard | «Объект не найден» | no retry |
CRM_PROPERTY_OBJECT_NOT_AVAILABLE | 409 | статус объекта Продан | «Объект уже продан» | no retry |
CRM_INSTALLMENT_PRESET_NOT_FOUND | 404 | пресет не найден | «Программа рассрочки не найдена» | no retry |
CRM_INSTALLMENT_PRESET_FORBIDDEN | 409 | пресет чужого застройщика/архивирован | «Программа недоступна» | no retry |
CRM_DEAL_PIPELINE_NOT_CONFIGURED | 409 | нет живого этапа группы Application у застройщика | «Воронка застройщика не настроена» | no retry |
CHESSBOARD_UNAVAILABLE | 503 | chessboard недоступен | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
- Новые optional поля запроса/ответа — additive, без breaking; удаление/переименование/смена типа — breaking
(migration + rollout). Unknown enum
PaymentMethod—ValidationError. - Logs:
deal created(DealId,LeadId,TenantId,PropertyObjectId,Status; ФИО/PII не пишутся); metricscrm_deals_created_total{payment_method,status}(status=draft|application), latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy сразу в Application (гейт выполнен). Given активный лид, свободный объект, подтверждённое
инвестором намерение по объекту, одобренная фиксация пары «инвестор × застройщик» и заданный способ оплаты;
When
POST; Then201, сделкаApplication, участникиInvestorPrimary/Executor/SellManager/PinManager,IntentId/IntentConfirmedAtзаполнены,ChatStatus = Pending, опубликованdeal-created(Status = Application). - Создание в Draft (гейт не выполнен). Given активный лид и свободный объект, но нет подтверждённого
намерения и/или одобренной фиксации; When
POST(PaymentMethodможно не передавать); Then201, сделкаDraft, участникиInvestorPrimary/Executor/PinManager(безSellManager), при Flow B создано намерениеAgentProposed, сделка невидима застройщику, опубликованdeal-created(Status = Draft). - Без способа оплаты при выполненном гейте (INV-DEAL-1). Given гейт выполнен, но
PaymentMethodне задан; WhenPOST; Then422 CRM_DEAL_PAYMENT_METHOD_REQUIRED. - Installment без пресета. Given
PaymentMethod = InstallmentбезInstallmentPresetId; WhenPOST; Then422 CRM_DEAL_INSTALLMENT_PRESET_REQUIRED. - Проданный объект. Given статус объекта
Продан; WhenPOST; Then409 CRM_PROPERTY_OBJECT_NOT_AVAILABLE. - Источник недоступен. Given
chessboardне отвечает; WhenPOST; Then503 CHESSBOARD_UNAVAILABLE(retry). - Воронка не настроена. Given у застройщика нет живого этапа группы
Application; WhenPOST; Then409 CRM_DEAL_PIPELINE_NOT_CONFIGURED. - Неактивный лид. Given
lead.Status != Active; WhenPOST; Then409 CRM_LEAD_NOT_ACTIVE.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (7): POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application), POST /api/agent/crm/deals/{dealId}/members — Добавить участника сделки, POST /api/agent/crm/leads/{leadId}/booking-intents — Агент предлагает объект клиенту, POST /api/developer/crm/fixations/{fixationId}/approve — Одобрить фиксацию (уникальная / не уникальная), POST /api/investor/crm/leads/{leadId}/booking-intents — Инвестор добавляет объект-намерение, POST /api/investor/crm/leads/{leadId}/booking-intents/{intentId}/confirm — Инвестор подтверждает предложенное намерение, PUT /api/agent/crm/deals/{dealId}/members/{memberId}/role — Сменить роль участника сделки
- Use Cases (4): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки, UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика)
- События (1): Event Contract: pin.crm.deal-created (DealCreatedEvent)
- scenarios (1): UC-OVR-005. От лида до сделки (4 фазы)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal.md (дом полей,
DealStatus.Draft, INV-DEAL-1/2/11,ParticipantRole,Money), lead.md (намерение по объекту,ConfirmedByInvestor,InvestorAdded/AgentProposed), lead.md (LeadEventType.DealCreated,LeadDealRefPayload, INV-LEAD-2), pipeline-step.md (начальный этап группыApplication), purchase-program.md (enumPaymentMethod,InstallmentPresetEntity), deal-timeline-event.md (записьDealCreated). - Событие: deal-created.md (
pin.crm.deal-created.v1); соседние — deal-stage-changed.md, deal-members-changed.md. - Вовлечение застройщика из
Draft: engage-developer.md (Draft → Application); авторизация работы с клиентом — fixation-request.md; карта областей — README контекста. - ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first.