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

POST /api/agent/crm/leads — Создать лид вручную

Агент заводит лид из карточки клиента (/crm/clients/[clientId]) с формой квалификации — ручной вход воронки в дополнение к автосозданию лидов консюмерами заявок/рефералов и при фиксации (Source = Manual, lead.md).

Назначение

Из карточки существующего зарегистрированного клиента создать активную связь «инвестор ↔ агент» с «шапкой хотелок» (бюджет, срочность, цель, способ оплаты — форма квалификации). Инвариант единственного активного лида пары (INV-CRM-001) не даёт задвоить воронку.

Инвариант

INV-CRM-001: у пары (агент, инвестор) может быть не более одного активного лида одновременно — попытка создать второй завершается 409 CRM_LEAD_ACTIVE_PAIR_EXISTS, новый лид не создаётся.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Агент; Руководитель агентства — по record-level своего TenantId)
Auth policy / Permissionscrm.leads.manage
Scope (RBAC)организация (агентство)
Record-levelсоздатель карточки клиента (client.OriginAgentUserId = actor) либо руководитель агентства (весь TenantId)
Tenant isolationTenantId = AgencyCompanyId в каждом запросе (ADR-0052)
Auditcreate (CreatedByUserId = actor)
Доступ

Permission crm.leads.manage даёт право на действие только в пределах своего TenantId (агентство); внутри tenant record-level сужает круг до создателя карточки клиента либо руководителя агентства (весь TenantId).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/leads
Success status201 (создан)
Idempotency headerне применяется (бизнес-дедуп по INV-CRM-001)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаAgencyCompanyId, RequiredTenant isolation
ClientIdbodyGuidДа!= Guid.Empty; существует в TenantId, Status = Active, InvestorUserId != null
WishesbodyLeadWishesModel?Нетформа квалификации; если задан — правила VO нижеPII нет (id/суммы)

LeadWishesModel (проекция VO LeadWishes — дом lead.md)

ПолеТипОбязательностьВалидация / Ограничения
BudgetNullableRange<decimal>Нет{Min, Max}; каждая граница >= 0; Min <= Max
BudgetCurrencyIdshort?Нетссылка на справочник валют handbook; не задан → валюта RUB (RUB-only, ADR-0048); неизвестная/архивная запись → VALIDATION_FAILED
Urgencystring? (enum LeadUrgency)Нетодно из значений enum строкой
PurchaseGoalstring? (enum PurchaseGoal)Нетодно из значений enum строкой
PreferredPaymentMethodstring? (enum PaymentMethod)Нетenum строкой (дом — purchase-program.md)
Commentstring?Нетvarchar(2000)

Желаемый город (CityId + CityName) при создании не задаётся — он появляется правкой шапки хотелок update-lead-wishes.md (там же — строгая валидация города через справочник, CRM_CITY_UNKNOWN).

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

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаLeadEntity.IdИдентификатор лида.
ClientIdGuidДаLeadEntity.ClientIdКарточка клиента.
OriginAgentUserIdGuidДаLeadEntity.OriginAgentUserIdАгент-создатель (атрибуция); при ручном создании = актор.
ExecutorAgentUserIdGuidДаLeadEntity.ExecutorAgentUserIdТекущий исполнитель — агент-сторона пары (при создании = актор).
PrimaryInvestorUserIdGuidДаLeadEntity.PrimaryInvestorUserIdОсновной инвестор — сторона пары.
Statusstring (enum LeadStatus)ДаLeadEntity.StatusActive для нового лида.
Sourcestring (enum LeadSource)ДаLeadEntity.SourceManual.
WishesLeadWishesModelДаLeadEntity.WishesСохранённая шапка хотелок.
ExpiresAtDateTimeOffsetДаLeadEntity.ExpiresAtCreatedAt + 1 месяц.
ExtensionsCountintДаLeadEntity.ExtensionsCount0 при создании.
CreatedAtDateTimeOffsetДаLeadEntity.CreatedAtМомент создания.

Списки лидов агента — отдельные query-методы; здесь ответ скалярный (одна карточка).

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент (permission crm.leads.manage) в tenant своего агентства (TenantId = AgencyCompanyId).
  2. Проверка входа и предусловий.
    • формат: ClientId != Guid.Empty; при обоих границах бюджета Min <= Max; Urgency/PurchaseGoal/ PreferredPaymentMethod — из допустимых enum; Comment ≤ 2000; BudgetCurrencyId (если задан) резолвится в справочнике валют handbook (resolve-references.md) — иначе ошибка валидации;
    • клиент существует в TenantId — иначе CRM_CLIENT_NOT_FOUND;
    • клиент принадлежит tenant актора и record-level (владелец либо руководитель агентства) — иначе CRM_CLIENT_FORBIDDEN;
    • клиент активен (Status = Active) — иначе CRM_CLIENT_ARCHIVED;
    • клиент зарегистрирован (InvestorUserId != null) — иначе CRM_LEAD_INVESTOR_REQUIRED (лид создаётся только для зарегистрированного инвестора).
  3. Что читаем. Карточку клиента — источник сторон пары (агент-исполнитель = актор → OriginAgentUserId/ ExecutorAgentUserId; PrimaryInvestorUserId = client.InvestorUserId) и проверки INV-CLI-2.
  4. Бизнес-правила (INV-CRM-001). Если у пары (агент, инвестор) уже есть активный лид — отказ CRM_LEAD_ACTIVE_PAIR_EXISTS. Иначе создать лид пары с Source = Manual, ExpiresAt = now + 1 месяц, собранной шапкой хотелок (пустой VO, если Wishes = null), начальным составом участников (Executor = актор, InvestorPrimary = client.InvestorUserId) — переход [*] → Active. У клиента проставить ActiveLeadId = lead.Id (INV-CLI-2) и LastActivityAt = now.
  5. Что меняется. Появляется запись лида; у клиента обновляются ActiveLeadId и LastActivityAt — в одном согласованном изменении.
  6. События. Публикуется LeadCreatedEvent → топик pin.crm.lead-created.v1 (ContractVersion = 1, key = LeadId), payload {LeadId, TenantId, OriginAgentUserId, ExecutorAgentUserId, PrimaryInvestorUserId, ClientId, Source = Manual, ReferralLinkId = null, CreatedAt, ExpiresAt} — через transactional outbox; контракт — lead-created.md.
  7. Результат. Карточка созданного лида → HTTP 201. При гонке INV-CRM-001 (параллельная вставка второго активного лида пары) — CRM_LEAD_ACTIVE_PAIR_EXISTS.

Диаграмма

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

Предусловия. Актор — агент/руководитель с crm.leads.manage, TenantId = AgencyCompanyId. Клиент существует в tenant, Status = Active, зарегистрирован (InvestorUserId != null). У пары (агент, инвестор) нет активного лида (INV-CRM-001).

Постусловие (успех). Появился LeadEntity: Status = Active, Source = Manual, ExpiresAt = now + 1 месяц, ExtensionsCount = 0, шапка хотелок сохранена (пустой VO при Wishes = null). У клиента проставлены ActiveLeadId = lead.Id (INV-CLI-2) и LastActivityAt = now. Опубликован LeadCreatedEvent (pin.crm.lead-created.v1, key LeadId); потребители (notifications — уведомление агенту, referral — подтверждение возможной привязки) реагируют асинхронно.

Постусловие (отказ). Ни лида, ни мутации клиента (ActiveLeadId/LastActivityAt), ни события — состояние без изменений.

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

ТипДетали
Интеграционные событияLeadCreatedEventpin.crm.lead-created.v1 (Source = Manual), outbox
Внешние вызовырезолв BudgetCurrencyId в справочнике handbook — только если поле задано (InvestorUserId уже в карточке клиента)
Проекции / поискclients.ActiveLeadId/LastActivityAt обновляются; проекция клиента (ClientSearchDocument) обновляется по LastActivityAt
Уведомленияопосредованно: консюмер lead-created в notifications (для Manual уведомление агенту может подавляться политикой)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат ClientId/Wishes (бюджет, enum, длины)«Проверьте корректность полей формы»no retry
CRM_CLIENT_NOT_FOUND404клиент не найден в TenantId«Клиент не найден»no retry
CRM_CLIENT_FORBIDDEN403клиент чужого агента/tenant«Нет доступа к клиенту»no retry
CRM_CLIENT_ARCHIVED409client.Status != Active«Клиент архивирован»no retry
CRM_LEAD_INVESTOR_REQUIRED409client.InvestorUserId == null«Клиент ещё не зарегистрирован»no retry
CRM_LEAD_ACTIVE_PAIR_EXISTS409активный лид пары уже есть (INV-CRM-001: pre-check или гонка)«Активный лид уже существует»no retry (перейти к существующему)

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

  • Новые optional поля Wishes — additive, без breaking; удаление/переименование/смена типа полей запроса или ответа — breaking (migration + rollout). Unknown enum-значения WishesValidationError.
  • Logs: lead created (LeadId, ClientId, TenantId; PII/Wishes не пишутся); metrics crm_leads_created_total{source="manual"}, latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given активный зарегистрированный клиент без активного лида пары; When POST; Then 201, лид Active, ExpiresAt = now + 1 месяц, у клиента проставлен ActiveLeadId, опубликован lead-created.
  • Дубль пары (INV-CRM-001). Given у пары (агент, инвестор) уже есть активный лид; When создание; Then 409 CRM_LEAD_ACTIVE_PAIR_EXISTS, новый лид не создан.
  • Не зарегистрирован. Given client.InvestorUserId == null; When создание; Then 409 CRM_LEAD_INVESTOR_REQUIRED.
  • Архивный клиент. Given client.Status != Active; When создание; Then 409 CRM_CLIENT_ARCHIVED.
  • Валидация хотелок. Given Wishes.Budget.Min > Budget.Max или неизвестный enum; When создание; Then 400 VALIDATION_FAILED.
  • Гонка. Given два параллельных создания на одну пару; When оба прошли pre-check; Then появляется ровно один лид, второй — 409 CRM_LEAD_ACTIVE_PAIR_EXISTS.

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

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

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