UC-CRM-001. Создание лида (заявка / реферал / вручную)
Назначение
Сценарий появления первой активной связи «инвестор ↔ агент»: создаётся лид LeadEntity и, при
необходимости, его родительская карточка клиента ClientEntity. Три точки входа — заявка с витрины
(консультация/бронирование), реферальная привязка и ручное создание агентом. Дом полей — lead.md
и client.md; контракт события — lead-created.md.
| Поле | Значение |
|---|
| ID | UC-CRM-001 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|
| Гость (витрина) | нет (публичный гейт Public) | null (ADR-0052) | не применяется |
| Инвестор (витрина) | авторизован; своя активность | null (инвестор без tenant) | свои заявки |
| Агент (вручную) | crm.leads.manage | AgencyCompanyId | свои клиенты (ExecutorAgentUserId = actor) |
| System (реферальная привязка / lazy-reg) | system-actor | scope записи = AgencyCompanyId/ПИН | не применяется |
Предусловие
- Для заявки с витрины: заполнена форма (ФИО + телефон; для бронирования — обязателен
PropertyObjectId).
- Для ручного создания: агент авторизован и принадлежит агентству (
AgencyCompanyId).
identity доступен (lazy-регистрация инвестора по телефону, RegistrationSource = LazyRegistration);
для заявки на бронирование доступен chessboard (валидность PropertyObjectId).
Триггер
| Поле | Значение |
|---|
| Источник | UI-действие (витрина / ЛК агента) → вызов API; либо событие шины (реферал) |
| Триггерящий элемент | Публичная заявка на консультацию/бронирование, ручное создание лида агентом, событие pin.referral.lead-attachments.v1 |
Основной Поток
Основная нить — публичная заявка с витрины (гость/инвестор). Ветки «реферал» и «вручную» — в альтернативах
A2/A3; общий хвост (get-or-create клиента и лида, лента, публикация события) одинаков для всех точек входа.
- Контекст и доступ. Публичный гейт разрешает анонимный вызов. Тип заявки — консультация
(
Source = ConsultationRequest) либо бронирование (Source = BookingRequest).
- Проверка входа. ФИО не пустое,
PhoneNumber — формат E.164; для бронирования — задан
PropertyObjectId. Нарушение → VALIDATION_ERROR. Повторная публичная заявка тем же телефоном в окне
дедупликации → DUPLICATE_PHONE (окно — 24 ч, для booking-заявок сверяется по паре телефон+объект, см.
UC-BKG-001). Ручное создание агентом (A2) дедуп-окно не
проходит — агент осознанно заводит клиента.
- Lazy-регистрация инвестора. Резолв
InvestorUserId в identity по нормализованному телефону:
существующий пользователь — reuse; иначе создаётся lazy-профиль (RegistrationSource = LazyRegistration,
identity/domain/user.md). Недоступность identity → IDENTITY_UNAVAILABLE.
- Определение агента-владельца. Реферальная привязка даёт
ExecutorAgentUserId и TenantId = AgencyCompanyId;
при её отсутствии — компания ПИН + случайный сотрудник ПИН (A4).
- get-or-create клиента. Ищем живую карточку
ClientEntity того же tenant по инвестору (INV-CLI-3),
резерв — по телефону (INV-CLI-1). Найдена → reuse (при необходимости привязываем инвестора); иначе
создаётся новая карточка со Status = Active (событие ClientCreated).
- Проверка INV-CRM-001. Ищем активный лид на пару (
ExecutorAgentUserId, PrimaryInvestorUserId) со статусом
LeadStatus.Active; параллельные заявки сериализуются. Найден → A1 (событие пишется в существующий
лид). Гонка на уникальном индексе активной пары ux_leads_active_pair → CRM_LEAD_ACTIVE_PAIR_EXISTS
(перечитать и завершиться идемпотентно).
- Создание лида. Фабрика лида проверяет INV-CRM-001 и создаёт
LeadEntity: TenantId = AgencyCompanyId
(или ПИН), Status = Active, ExpiresAt = CreatedAt + 1 месяц, Wishes = LeadWishes.Empty (публичная
заявка; форму квалификации агент заполняет позже — INV-LEAD-5); событие LeadCreated. Клиенту
проставляется денормализованный ActiveLeadId (INV-CLI-2).
- Событие ленты. В ленту лида добавляется
LeadEventEntity: тип по Source
(ConsultationRequested / BookingRequested), типизированный payload
(LeadConsultationRequestedPayload / LeadBookingRequestedPayload, дом — lead.md),
обновляется Lead.LastEventAt. Событие LeadEventAdded.
- Согласованность. Клиент + лид + событие ленты + запись в outbox фиксируются атомарно в одном
согласованном изменении.
- Публикация. Интеграционное событие
LeadCreatedEvent (топик pin.crm.lead-created.v1,
AggregateId = LeadId) публикуется через outbox — только при создании нового лида (в A1 — no-op).
Потребители: notifications (уведомление агенту), referral (подтверждение привязки при Source = Referral).
- Результат. Возвращаются
LeadId, ClientId, Status, ExpiresAt.
Диаграмма
Альтернативные Потоки
A1. У пары уже есть активный лид (INV-CRM-001)
- Шаг 6 нашёл активный лид (
Status = Active) на пару (ExecutorAgentUserId, PrimaryInvestorUserId).
- Новый
LeadEntity не создаётся; событие пишется в существующую ленту, обновляется LastEventAt.
LeadCreatedEvent не публикуется (не новый лид — идемпотентный no-op). При гонке на
ux_leads_active_pair — перечитать активный лид и завершиться идемпотентно.
Завершается без создания лида (создан только LeadEventEntity).
A2. Ручное создание агентом (Source = Manual)
- Актор — агент (
crm.leads.manage), TenantId = AgencyCompanyId.
- Клиент выбирается из базы агента или создаётся (
InvestorUserId может быть null до регистрации);
lazy-регистрация (шаг 3) не выполняется.
Wishes заполняется из формы квалификации агента (не Empty). Далее — общий хвост шагов 6–11.
A3. Реферальная привязка (Source = Referral)
- Событие
pin.referral.lead-attachments.v1 обрабатывает реферальный консюмер (system-actor) — весь
get-or-create + INV-CRM-001 + добавление события ленты описаны в
referral-registered.md (не дублируется здесь).
ReferralLinkId заполнен; LeadCreatedEvent несёт Source = Referral → referral подтверждает привязку.
Возвращается к общему хвосту (шаги 7–11) внутри обработки события.
A4. Реферальной привязки нет (UC-2)
- Шаг 4 не нашёл реферальную привязку/агента-владельца.
TenantId = CompanyId компании ПИН; ExecutorAgentUserId = случайный сотрудник ПИН.
- Далее — общий хвост; лид «живёт» в компании ПИН, уведомление уходит выбранному сотруднику.
Постусловие
- Создана карточка клиента (
ClientEntity, если не было) со Status = Active.
- Создан лид (
LeadEntity, Status = Active, ExpiresAt = CreatedAt + 1 месяц) — кроме A1, где лид
переиспользуется.
- В ленту добавлено событие (
LeadEventEntity) соответствующего типа; обновлён Lead.LastEventAt.
- Опубликован
pin.crm.lead-created.v1 — только при создании нового лида (не в A1).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
VALIDATION_ERROR | 400 | Пустое ФИО / неверный телефон / бронирование без PropertyObjectId | Показать ошибки формы |
DUPLICATE_PHONE | 409 | Повторная публичная заявка тем же телефоном в окне дедупликации | Сообщить, что заявка уже принята |
CRM_CLIENT_PHONE_DUPLICATE | 409 | Телефон занят другой живой карточкой агентства (INV-CLI-1) | Ретрай / ручной merge (забота) |
CRM_LEAD_ACTIVE_PAIR_EXISTS | — (internal) | Гонка ux_leads_active_pair при создании | Перечитать и завершиться идемпотентно (A1) |
IDENTITY_UNAVAILABLE | 503 | identity недоступен при lazy-регистрации | Ретрай позже |
CHESSBOARD_UNAVAILABLE | 503 | chessboard недоступен при валидации PropertyObjectId (бронирование) | Ретрай позже |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|
ClientCreated | Создание новой карточки клиента | внутренний диспетчер |
LeadCreated | Создание нового лида ([*] → Active) | внутренний диспетчер → сборка LeadCreatedEvent |
LeadEventAdded | Добавление LeadEventEntity в ленту | внутренний диспетчер |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|
LeadCreatedEvent | pin.crm.lead-created.v1 | Только при создании нового лида ([*] → Active); A1 — no-op | notifications (уведомление агенту), referral (подтверждение привязки при Source = Referral) |
Контракт/payload — lead-created.md (PII осознанно исключён). Публикация — через
outbox, атомарно с созданием агрегата.
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | Публичная заявка «консультация» создаёт клиент + лид + событие ConsultationRequested | Integration-тест публичного метода; проверить 3 записи + outbox |
| AC2 | Заявка на бронирование создаёт лид Source = BookingRequest, событие BookingRequested с PropertyObjectId | Тест заявки на бронирование; проверить payload события ленты |
| AC3 | У пары уже есть Active-лид → новый лид не создаётся, событие в существующий, lead-created не публикуется | Тест A1: два вызова подряд, одна строка leads, две — lead_events |
| AC4 | Нет реф-привязки → TenantId = ПИН, ExecutorAgentUserId = сотрудник ПИН | Тест A4: проверить tenant/agent лида |
| AC5 | ExpiresAt = CreatedAt + 1 месяц | Assert на созданном лиде |
| AC6 | Клиент + лид + событие + outbox атомарны; сбой outbox откатывает лид | Тест с инъекцией сбоя транзакции |
| AC7 | Публичная заявка тем же телефоном в окне даёт 409 DUPLICATE_PHONE | Тест повторной заявки тем же телефоном |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы