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

POST /api/public/crm/consultation-requests — Заявка на консультацию/презентацию

Назначение

Публичная точка входа «инвестор с витрины просит консультацию/презентацию» (UC-1/UC-2): принимает форму «Фамилия, Имя, Отчество (опционально), Телефон» + необязательный контекст (подборка, объект, желаемое время) и заводит/обновляет воронку агента: клиент → лид → событие ленты ConsultationRequested. Заявка не создаёт отдельной сущности «запрос презентации»: результат — lazy-регистрация инвестора, карточка клиента, лид и событие ленты (lead.md); подборка/объект/желаемое время — опциональные подсказки для агента. PreferredAtмягкая подсказка желаемого времени: запись в календарь агента не создаётся и время никак не резервируется, агент связывается с клиентом и договаривается сам. Жёсткая запись на конкретное время — отдельный поток самозаписи на слот агента (book-slot.md, контекст calendar): там время гарантируется забронированным слотом, а CRM получает клиента/лид и ленту асинхронно по событиям pin.calendar.appointment-*.v1 (calendar-appointments.md). Обе точки входа сосуществуют: заявка без выбора времени — здесь, выбор конкретного слота — в календаре. Повторная заявка с тем же телефоном в окне дедупа отклоняется кодом DUPLICATE_PHONE.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areapublic (гейт витрины: JWT инвестора или реферальная cookie)
Feature groupconsultation
Статусdraft
Документdocs/06-services/crm/api/public/create-consultation-request.md

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

ПроверкаЗначение
Actor typeAnonymous (гейт витрины: валидная реф-cookie referralUrl) или User (Инвестор с JWT)
Auth policy / Permissionsnone (публичный метод); rate limit по IP+телефону
Scope (RBAC)не применяется (актор — вне tenant)
Record-levelне применяется
Tenant isolationTenantId целевого лида = AgencyCompanyId, вычисляется правилами привязки (шаг 4); запрос выполняется в system-scope
Auditcreate (CreatedByUserId = null для анонима / UserId инвестора); PII формы в логи не пишется
Доступ

Публичный метод без авторизации: доступен и анониму с валидной реф-cookie referralUrl, и авторизованному инвестору (JWT опционален). Rate limit — по IP и телефону; PII формы (ФИО, телефон) в логи не пишется.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/public/crm/consultation-requests
Success status201 (создано)
Idempotency headerне применяется (бизнес-дубль-детект по телефону → 409 DUPLICATE_PHONE, шаг 2)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
LastNamebodystringДа1..128, не пустое после trimPII: в логи не пишется
FirstNamebodystringДа1..128, не пустое после trimPII
MiddleNamebodystring?Нет0..128 (отчество опционально)PII
PhoneNumberbodystringДанормализация и формат E.164 (^\+[1-9]\d{9,14}$), max 32PII; маска +7***NNNN в логах
SelectionIdbodyGuid?Нетопционально; логическая ссылка на selections, синхронно не проверяется
PropertyObjectIdbodyGuid?Нетопционально; логическая ссылка на объект недвижимости chessboard, синхронно не проверяется
PreferredAtbodyDateTimeOffset?Нетопционально; если задан — не в прошлом
ReferralCodecookie referralUrlstring?Нетвалидируется контекстом referral (шаг 4)
ActorUserIdidentity claimsGuid?НетJWT инвестора, если авторизован

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

ПолеТипОбязательностьИсточникMaskingОписание
RequestIdGuidДаидентификатор события ConsultationRequested в ленте лидаИдентификатор принятой заявки.
ReceivedAtDateTimeOffsetДамомент приёма события лентыМомент приёма.
Ответ

Идентификаторы лида/клиента наружу не возвращаются (внутренние данные агентства, client.md). Флага «дубликат» нет: дубль-детект — жёсткий 409 (шаг 2), а не идемпотентный 200.

Алгоритм

  1. Контекст и доступ. Публичный метод; актор Anonymous/User (JWT опционален), гейт витрины — JWT инвестора или реф-cookie; rate limit по IP+телефону.
  2. Проверка входа и предусловий.
    • дубль-детект: нормализовать PhoneNumber → E.164; если у живого клиента с этим телефоном есть событие ConsultationRequested за окно ConsultationDedupWindow (параметр программы, default 24 ч) в активном лиде (Status = Active) → DUPLICATE_PHONE → HTTP 409 (повторная форма отклонена, дальнейшие шаги не выполняются);
    • формат: длины ФИО (1..128), формат телефона E.164, MiddleName опционально ≤ 128; PreferredAt, если задан, не в прошлом → иначе VALIDATION_FAILED;
    • PropertyObjectId/SelectionId — логические ссылки-подсказки, синхронно не проверяются (консультация — мягкий lead-gen intent; агент уточняет объект вручную).
  3. Lazy-регистрация инвестора. Вызов identity «get-or-create user by phone» (RegistrationSource = LazyRegistration; имя записывается, если у пользователя пустое) → PrimaryInvestorUserId. Если JWT-инвестор авторизован и его телефон совпадает — используется ActorUserId без вызова identity.
  4. Бизнес-правила и переходы. Определить агентскую сторону (правила привязки — контекст referral): валидная реф-ссылка с владельцем-агентом → ExecutorAgentUserId = владелец (он же снапшотится в иммутабельный OriginAgentUserId), TenantId = его AgencyCompanyId; иначе (ссылки нет / ссылка инвестора) → TenantId = компания ПИН, ExecutorAgentUserId = случайный активный сотрудник ПИН (OriginAgentUserId = он же) (UC-2). Далее get-or-create цепочки:
    • ClientEntity по паре (TenantId, PhoneNumber) (unique, INV-CLI-1); нет → создаётся с Source = ConsultationRequest, ФИО {LastName, FirstName, MiddleName}; есть, но PrimaryInvestorUserId = null — проставляется (INV-CLI-3);
    • активный LeadEntity по паре (ExecutorAgentUserId, PrimaryInvestorUserId, Status = Active) (unique, INV-CRM-001); нет → создаётся с Source = ConsultationRequest, ExpiresAt = now + 1 месяц, пустыми хотелками, ReferralLinkId из реф-cookie (если была);
    • в ленту лида добавляется событие ConsultationRequested (Payload = LeadConsultationRequestedPayload { SelectionId?, PropertyObjectId?, PreferredAt? }, INV-LEAD-2); у клиента обновляются LastActivityAt и привязка активного лида (INV-CLI-2).
  5. Что меняется. Клиент (создан/обновлён), лид (создан/переиспользован) и событие ленты изменяются в одной согласованной записи. Гонка INV-CLI-1 (два параллельных первых обращения одного телефона) → конфликт уникальности → CRM_CLIENT_PHONE_DUPLICATE (ретрай перечитает клиента).
  6. Событие. Если лид создан — публикуется интеграционное LeadCreatedEvent → topic pin.crm.lead-created.v1 (ContractVersion = 1), payload { LeadId, TenantId, OriginAgentUserId, ExecutorAgentUserId, PrimaryInvestorUserId, ClientId, Source = ConsultationRequest, ReferralLinkId?, CreatedAt, ExpiresAt } (контракт lead-created.md). Существующий (переиспользованный) лид интеграционных событий не порождает (агент увидит новое событие ленты по LastEventAt).
  7. Результат. { RequestId, ReceivedAt } → HTTP 201.

Диаграмма

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

ТипДетали
Интеграционные событияLeadCreatedEventpin.crm.lead-created.v1 (только если лид новый)
Внешние вызовыidentity get-or-create по телефону (timeout 3 c, retry 1); referral — валидация кода привязки. chessboard не вызывается (PropertyObjectId синхронно не проверяется)
Проекции / поискпоисковая проекция клиентов обновляется; у клиента обновляются привязка активного лида и LastActivityAt
Уведомленияопосредованно: notifications — консюмер lead-created (уведомление агенту о новом лиде)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат ФИО/телефона/PreferredAt«Проверьте корректность полей формы»no retry
DUPLICATE_PHONE409недавняя (в окне) заявка на консультацию с этим телефоном«Заявка с этим телефоном уже принята»no retry
CRM_CLIENT_PHONE_DUPLICATE409гонка INV-CLI-1 (конфликт уникальности при вставке клиента)«Повторите попытку»retry (перечитка клиента)
IDENTITY_UNAVAILABLE503identity недоступен на шаге 3«Сервис временно недоступен»retry
Коды ошибок

Код дубль-детекта публичной формы — DUPLICATE_PHONE (не из семейства CRM_*); остальные коды — платформенные CRM_*/VALIDATION_FAILED.

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

  • Обязательные поля формы (ФИО, телефон) и ответ 409 DUPLICATE_PHONE — стабильный публичный контракт; SelectionId/PropertyObjectId/PreferredAt — additive optional. Удаление/переименование полей или смена типа — breaking (migration + rollout). Unknown enum-значений в запросе нет.
  • Logs: consultation-request received/rejected-duplicate/created (RequestId, TenantId; телефон — маской, ФИО не пишется); metrics crm_consultation_requests_total{result}, latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard Grafana pin-crm.

Постусловия

После 201 гарантированно: существует зарегистрированный (lazy) пользователь-инвестор с этим телефоном; живой клиент в базе агентской стороны (TenantId); активный лид пары (ExecutorAgentUserId, PrimaryInvestorUserId) со Status = Active; в ленте лида — событие ConsultationRequested; у клиента обновлены LastActivityAt и ActiveLeadId. Если лид создан впервые — опубликовано LeadCreatedEvent. Отдельной сущности «запрос презентации» не появляется. Промежуточное состояние (eventual consistency): инвестор получает 201 сразу; агент видит новый лид в списке по LastEventAt после commit, уведомление — после обработки lead-created консюмером notifications.

Acceptance Criteria

  • AC-1 (happy — новый лид). Дано: клиента/лида с этим телефоном нет. Когда: заявка на консультацию. Тогда: lazy-регистрация инвестора, создан клиент и лид (Source = ConsultationRequest), событие ленты ConsultationRequested с опциональными подсказками (SelectionId/PropertyObjectId/PreferredAt), публикуется LeadCreatedEvent, ответ 201.
  • AC-2 (дубль-детект). Дано: у живого клиента с этим телефоном есть ConsultationRequested в окне ConsultationDedupWindow (default 24 ч) в активном лиде. Когда: повторная заявка. Тогда: отказ DUPLICATE_PHONE (409), дальнейшие шаги не выполняются.
  • AC-3 (PreferredAt в прошлом). Дано: PreferredAt раньше текущего момента. Когда: заявка. Тогда: отказ VALIDATION_FAILED (400).
  • AC-4 (реф-привязка к агенту). Дано: валидная реф-cookie с владельцем-агентом. Когда: заявка. Тогда: TenantId = AgencyCompanyId агента, ExecutorAgentUserId (= OriginAgentUserId) = владелец ссылки, ReferralLinkId заполнен.
  • AC-5 (переиспользование лида). Дано: активный лид пары уже существует (вне окна дедупа). Когда: заявка. Тогда: событие ленты ConsultationRequested добавлено в существующий лид, LeadCreatedEvent не публикуется.
  • AC-6 (identity недоступен). Дано: identity не отвечает. Когда: заявка. Тогда: отказ IDENTITY_UNAVAILABLE (503, retriable), заявка не принята.

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

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

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

  • Сущности: client.md (Source = ConsultationRequest, INV-CLI-1/2/3), lead.md (LeadSource.ConsultationRequest, LeadEventType.ConsultationRequested, payload LeadConsultationRequestedPayload).
  • Событие: lead-created.md (pin.crm.lead-created.v1, Source = ConsultationRequest).
  • Смежный вход воронки: create-booking-request.md (заявка на бронирование, UC-3); карта областей — README контекста.
  • Жёсткая запись на время (слот агента, различие потоков — см. «Назначение»): book-slot.md (calendar); CRM-эффект записи — консюмер calendar-appointments.md.
  • Сценарии UC-1/UC-2 — docs/02-architecture/scenarios/; ADR: 0052 tenancy, 0056 simplicity-first.