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 area | public (гейт витрины: JWT инвестора или реферальная cookie) |
| Feature group | consultation |
| Статус | draft |
| Документ | docs/06-services/crm/api/public/create-consultation-request.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous (гейт витрины: валидная реф-cookie referralUrl) или User (Инвестор с JWT) |
| Auth policy / Permissions | none (публичный метод); rate limit по IP+телефону |
| Scope (RBAC) | не применяется (актор — вне tenant) |
| Record-level | не применяется |
| Tenant isolation | TenantId целевого лида = AgencyCompanyId, вычисляется правилами привязки (шаг 4); запрос выполняется в system-scope |
| Audit | create (CreatedByUserId = null для анонима / UserId инвестора); PII формы в логи не пишется |
Публичный метод без авторизации: доступен и анониму с валидной реф-cookie referralUrl, и авторизованному
инвестору (JWT опционален). Rate limit — по IP и телефону; PII формы (ФИО, телефон) в логи не пишется.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/public/crm/consultation-requests |
| Success status | 201 (создано) |
| Idempotency header | не применяется (бизнес-дубль-детект по телефону → 409 DUPLICATE_PHONE, шаг 2) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
LastName | body | string | Да | 1..128, не пустое после trim | PII: в логи не пишется |
FirstName | body | string | Да | 1..128, не пустое после trim | PII |
MiddleName | body | string? | Нет | 0..128 (отчество опционально) | PII |
PhoneNumber | body | string | Да | нормализация и формат E.164 (^\+[1-9]\d{9,14}$), max 32 | PII; маска +7***NNNN в логах |
SelectionId | body | Guid? | Нет | опционально; логическая ссылка на selections, синхронно не проверяется | — |
PropertyObjectId | body | Guid? | Нет | опционально; логическая ссылка на объект недвижимости chessboard, синхронно не проверяется | — |
PreferredAt | body | DateTimeOffset? | Нет | опционально; если задан — не в прошлом | — |
ReferralCode | cookie referralUrl | string? | Нет | валидируется контекстом referral (шаг 4) | — |
ActorUserId | identity claims | Guid? | Нет | JWT инвестора, если авторизован | — |
Модель ответа ConsultationRequestResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
RequestId | Guid | Да | идентификатор события ConsultationRequested в ленте лида | — | Идентификатор принятой заявки. |
ReceivedAt | DateTimeOffset | Да | момент приёма события ленты | — | Момент приёма. |
Идентификаторы лида/клиента наружу не возвращаются (внутренние данные агентства,
client.md). Флага «дубликат» нет: дубль-детект — жёсткий 409 (шаг 2), а не
идемпотентный 200.
Алгоритм
- Контекст и доступ. Публичный метод; актор
Anonymous/User(JWT опционален), гейт витрины — JWT инвестора или реф-cookie; rate limit по IP+телефону. - Проверка входа и предусловий.
- дубль-детект: нормализовать
PhoneNumber→ E.164; если у живого клиента с этим телефоном есть событиеConsultationRequestedза окноConsultationDedupWindow(параметр программы, default 24 ч) в активном лиде (Status = Active) →DUPLICATE_PHONE→ HTTP409(повторная форма отклонена, дальнейшие шаги не выполняются); - формат: длины ФИО (
1..128), формат телефона E.164,MiddleNameопционально ≤ 128;PreferredAt, если задан, не в прошлом → иначеVALIDATION_FAILED; PropertyObjectId/SelectionId— логические ссылки-подсказки, синхронно не проверяются (консультация — мягкий lead-gen intent; агент уточняет объект вручную).
- дубль-детект: нормализовать
- Lazy-регистрация инвестора. Вызов
identity«get-or-create user by phone» (RegistrationSource = LazyRegistration; имя записывается, если у пользователя пустое) →PrimaryInvestorUserId. Если JWT-инвестор авторизован и его телефон совпадает — используетсяActorUserIdбез вызова identity. - Бизнес-правила и переходы. Определить агентскую сторону (правила привязки — контекст
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).
- Что меняется. Клиент (создан/обновлён), лид (создан/переиспользован) и событие ленты изменяются в
одной согласованной записи. Гонка INV-CLI-1 (два параллельных первых обращения одного телефона) →
конфликт уникальности →
CRM_CLIENT_PHONE_DUPLICATE(ретрай перечитает клиента). - Событие. Если лид создан — публикуется интеграционное
LeadCreatedEvent→ topicpin.crm.lead-created.v1(ContractVersion = 1), payload{ LeadId, TenantId, OriginAgentUserId, ExecutorAgentUserId, PrimaryInvestorUserId, ClientId, Source = ConsultationRequest, ReferralLinkId?, CreatedAt, ExpiresAt }(контракт lead-created.md). Существующий (переиспользованный) лид интеграционных событий не порождает (агент увидит новое событие ленты поLastEventAt). - Результат.
{ RequestId, ReceivedAt }→ HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LeadCreatedEvent → pin.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_FAILED | 400 | формат ФИО/телефона/PreferredAt | «Проверьте корректность полей формы» | no retry |
DUPLICATE_PHONE | 409 | недавняя (в окне) заявка на консультацию с этим телефоном | «Заявка с этим телефоном уже принята» | no retry |
CRM_CLIENT_PHONE_DUPLICATE | 409 | гонка INV-CLI-1 (конфликт уникальности при вставке клиента) | «Повторите попытку» | retry (перечитка клиента) |
IDENTITY_UNAVAILABLE | 503 | identity недоступен на шаге 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; телефон — маской, ФИО не пишется); metricscrm_consultation_requests_total{result}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboard Grafanapin-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), заявка не принята.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: AppointmentEntity
- API (4): GET /api/public/crm/purchase-config — Конфигурация способов оплаты (витрина), POST /api/public/calendar/appointments — Самозапись на слот (бронирование), POST /api/public/crm/booking-requests — Заявка на бронирование объекта (витрина), POST /api/public/crm/payment-schedule — Серверный график платежей (витрина)
- Use Cases (2): UC-CAL-003. Инвестор бронирует слот с витрины, US-реестр контекста calendar (US-CAL)
- События (1): Event Contract: pin.calendar.appointment-created (AppointmentCreatedEvent)
- Консюмеры (1): Consumer: CRM Calendar Appointments Sync (crm-calendar-appointments)
- adr (1): ADR-0064 — контекст calendar (календарь и запись на показ/консультацию)
- scenarios (2): UC-OVR-001. Запрос консультации у агента недвижимости, UC-OVR-002. Запрос консультации у инвест-консультанта ПИН (+2a)
- index.md (1): Calendar Service
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: client.md (
Source = ConsultationRequest, INV-CLI-1/2/3), lead.md (LeadSource.ConsultationRequest,LeadEventType.ConsultationRequested, payloadLeadConsultationRequestedPayload). - Событие: 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.