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 area | agent |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/create-lead.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент; Руководитель агентства — по record-level своего TenantId) |
| Auth policy / Permissions | crm.leads.manage |
| Scope (RBAC) | организация (агентство) |
| Record-level | создатель карточки клиента (client.OriginAgentUserId = actor) либо руководитель агентства (весь TenantId) |
| Tenant isolation | TenantId = AgencyCompanyId в каждом запросе (ADR-0052) |
| Audit | create (CreatedByUserId = actor) |
Permission crm.leads.manage даёт право на действие только в пределах своего TenantId (агентство); внутри
tenant record-level сужает круг до создателя карточки клиента либо руководителя агентства (весь TenantId).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/leads |
| Success status | 201 (создан) |
| Idempotency header | не применяется (бизнес-дедуп по INV-CRM-001) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | AgencyCompanyId, Required | Tenant isolation |
ClientId | body | Guid | Да | != Guid.Empty; существует в TenantId, Status = Active, InvestorUserId != null | — |
Wishes | body | LeadWishesModel? | Нет | форма квалификации; если задан — правила VO ниже | PII нет (id/суммы) |
LeadWishesModel (проекция VO LeadWishes — дом lead.md)
| Поле | Тип | Обязательность | Валидация / Ограничения |
|---|---|---|---|
Budget | NullableRange<decimal> | Нет | {Min, Max}; каждая граница >= 0; Min <= Max |
BudgetCurrencyId | short? | Нет | ссылка на справочник валют handbook; не задан → валюта RUB (RUB-only, ADR-0048); неизвестная/архивная запись → VALIDATION_FAILED |
Urgency | string? (enum LeadUrgency) | Нет | одно из значений enum строкой |
PurchaseGoal | string? (enum PurchaseGoal) | Нет | одно из значений enum строкой |
PreferredPaymentMethod | string? (enum PaymentMethod) | Нет | enum строкой (дом — purchase-program.md) |
Comment | string? | Нет | varchar(2000) |
Желаемый город (CityId + CityName) при создании не задаётся — он появляется правкой шапки хотелок
update-lead-wishes.md (там же — строгая валидация города через справочник,
CRM_CITY_UNKNOWN).
Модель ответа LeadCardModel
Проекция созданного лида (дом полей — lead.md; ниже — только отдаваемая дельта).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LeadEntity.Id | — | Идентификатор лида. |
ClientId | Guid | Да | LeadEntity.ClientId | — | Карточка клиента. |
OriginAgentUserId | Guid | Да | LeadEntity.OriginAgentUserId | — | Агент-создатель (атрибуция); при ручном создании = актор. |
ExecutorAgentUserId | Guid | Да | LeadEntity.ExecutorAgentUserId | — | Текущий исполнитель — агент-сторона пары (при создании = актор). |
PrimaryInvestorUserId | Guid | Да | LeadEntity.PrimaryInvestorUserId | — | Основной инвестор — сторона пары. |
Status | string (enum LeadStatus) | Да | LeadEntity.Status | — | Active для нового лида. |
Source | string (enum LeadSource) | Да | LeadEntity.Source | — | Manual. |
Wishes | LeadWishesModel | Да | LeadEntity.Wishes | — | Сохранённая шапка хотелок. |
ExpiresAt | DateTimeOffset | Да | LeadEntity.ExpiresAt | — | CreatedAt + 1 месяц. |
ExtensionsCount | int | Да | LeadEntity.ExtensionsCount | — | 0 при создании. |
CreatedAt | DateTimeOffset | Да | LeadEntity.CreatedAt | — | Момент создания. |
Списки лидов агента — отдельные query-методы; здесь ответ скалярный (одна карточка).
Алгоритм
- Контекст и доступ. Операцию выполняет агент (permission
crm.leads.manage) в tenant своего агентства (TenantId = AgencyCompanyId). - Проверка входа и предусловий.
- формат:
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(лид создаётся только для зарегистрированного инвестора).
- формат:
- Что читаем. Карточку клиента — источник сторон пары (агент-исполнитель = актор →
OriginAgentUserId/ExecutorAgentUserId;PrimaryInvestorUserId=client.InvestorUserId) и проверки INV-CLI-2. - Бизнес-правила (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. - Что меняется. Появляется запись лида; у клиента обновляются
ActiveLeadIdиLastActivityAt— в одном согласованном изменении. - События. Публикуется
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. - Результат. Карточка созданного лида → 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), ни события — состояние
без изменений.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LeadCreatedEvent → pin.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_FAILED | 400 | формат ClientId/Wishes (бюджет, enum, длины) | «Проверьте корректность полей формы» | no retry |
CRM_CLIENT_NOT_FOUND | 404 | клиент не найден в TenantId | «Клиент не найден» | no retry |
CRM_CLIENT_FORBIDDEN | 403 | клиент чужого агента/tenant | «Нет доступа к клиенту» | no retry |
CRM_CLIENT_ARCHIVED | 409 | client.Status != Active | «Клиент архивирован» | no retry |
CRM_LEAD_INVESTOR_REQUIRED | 409 | client.InvestorUserId == null | «Клиент ещё не зарегистрирован» | no retry |
CRM_LEAD_ACTIVE_PAIR_EXISTS | 409 | активный лид пары уже есть (INV-CRM-001: pre-check или гонка) | «Активный лид уже существует» | no retry (перейти к существующему) |
Совместимость и наблюдаемость
- Новые optional поля
Wishes— additive, без breaking; удаление/переименование/смена типа полей запроса или ответа — breaking (migration + rollout). Unknown enum-значенияWishes—ValidationError. - Logs:
lead created(LeadId,ClientId,TenantId; PII/Wishesне пишутся); metricscrm_leads_created_total{source="manual"}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given активный зарегистрированный клиент без активного лида пары; When
POST; Then201, лидActive,ExpiresAt = now + 1 месяц, у клиента проставленActiveLeadId, опубликованlead-created. - Дубль пары (INV-CRM-001). Given у пары (агент, инвестор) уже есть активный лид; When создание; Then
409 CRM_LEAD_ACTIVE_PAIR_EXISTS, новый лид не создан. - Не зарегистрирован. Given
client.InvestorUserId == null; When создание; Then409 CRM_LEAD_INVESTOR_REQUIRED. - Архивный клиент. Given
client.Status != Active; When создание; Then409 CRM_CLIENT_ARCHIVED. - Валидация хотелок. Given
Wishes.Budget.Min > Budget.Maxили неизвестный enum; When создание; Then400 VALIDATION_FAILED. - Гонка. Given два параллельных создания на одну пару; When оба прошли pre-check; Then появляется ровно один
лид, второй —
409 CRM_LEAD_ACTIVE_PAIR_EXISTS.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: lead.md (дом полей, INV-CRM-001,
LeadWishes,LeadSource), client.md (ActiveLeadId, INV-CLI-2, регистрацияInvestorUserId). - Событие: lead-created.md (
pin.crm.lead-created.v1). - Смежный вход воронки: create-booking-request.md (автосоздание лида
Source = BookingRequest); карта областей — README контекста. - ADR: 0052 tenancy, 0056 simplicity-first.