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

POST /api/agent/crm/fixations — Отправить заявку на фиксацию

Агент запрашивает у выбранного застройщика закрепить клиента (ФИО + телефон) за собой до сделки. Точка входа агентской воронки: заявка создаётся только из лида (lead.md) в статусе Active, снимает иммутабельный snapshot ФИО/телефона из карточки клиента и выполняет авто-подсветку дублей у застройщика (полнотекстовый поиск по ФИО + точное совпадение телефона). Дом полей — fixation-request.md.

Назначение

Принять от агента лид-основание, выбранного застройщика и опциональный override хотелок, завести заявку на фиксацию (FixationRequestEntity) в статусе Pending. Агентские данные (агентство, агент, инвестор, клиент) берутся из лида; адресат — выбранный застройщик; ФИО/телефон фиксируются snapshot-ом из карточки клиента (client.md). Инвестор о фиксациях не знает.

Инвариант

INV-FIX-001: у тройки (агент, инвестор, застройщик) не может быть больше одной активной заявки на фиксацию одновременно (Status ∈ {Pending, FixedUnique, FixedNonUnique}) — попытка создать вторую завершается 409 CRM_FIXATION_ACTIVE_EXISTS, новая заявка не создаётся.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Агент); Operator (Менеджер ПИН — platform-scope, подача от имени агента)
Auth policy / Permissionscrm.fixations.manage (агентская сторона)
Scope (RBAC)организация (агентство): record-level по лиду
Record-levelАктор — активный участник лида (обычно Executor) либо руководитель компании-владельца (AgencyCompanyId = TenantId лида) / Менеджер ПИН
Tenant isolationлид читается в tenant актора (AgencyCompanyId); создаваемая фиксация TenantId = DeveloperCompanyId (застройщик-адресат, ADR-0052) — кросс-tenant запись санкционирована доменной моделью, выборка застройщика идёт по явному предикату
Auditcreate (CreatedByUserId = actor.UserId); телефон/ФИО клиента в логи не пишутся
Доступ

Кросс-tenant запись санкционирована доменной моделью: заявка создаётся в tenant'е выбранного застройщика (TenantId = DeveloperCompanyId), а актор проверяется не по этому tenant, а record-level предикатом по лиду — лид читается в tenant агентства актора (AgencyCompanyId), выборка застройщика идёт по явному предикату.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
LeadIdbodyGuidДа!= Guid.Empty; существующий лид Status = Active, принадлежит агентству актора
DeveloperCompanyIdbodyGuidДа!= Guid.Empty; существующий застройщик; становится FixationRequestEntity.TenantId
WishesOverridebodyLeadWishes? (owned VO — lead.md)Нетполя VO по контракту lead.md (Budget.Min <= Budget.Max; BudgetCurrencyId short? — валюта справочника handbook, не задан → RUB; CityId long? — город справочника, CityName снимается из resolve при записи; enum строкой); null → застройщику показываются Lead.WishesPII: Comment целиком в логи не пишется
ActorUserIdidentity claimsGuidДаJWT агента; = lead.ExecutorAgentUserId (кроме подачи Менеджером ПИН)
ActorAgencyCompanyIdidentity claimsGuidДаtenant агентства актора — record-level по лидуTenant isolation

WishesOverride — единственный typed nested-объект тела; его поля не дублируются здесь (дом — VO LeadWishes в lead.md).

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

Проекция созданной заявки (дельта дома fixation-request.md); агент видит свои же переданные данные, поэтому маскирование телефона не требуется.

ПолеТипОбязательностьИсточникMaskingОписание
FixationRequestIdGuidДаFixationRequestEntity.IdИдентификатор заявки.
Statusstring (enum FixationStatus)ДаStatusВсегда Pending на создании.
DeveloperCompanyIdGuidДаTenantIdЗастройщик-адресат.
LeadIdGuidДаLeadIdЛид-основание.
ClientIdGuidДаClientIdКарточка клиента агентства.
ClientNameFullName (owned — client.md)ДаClientName (snapshot)ФИО в заявке.
ClientPhoneNumberstringДаClientPhoneNumber (snapshot)Телефон в заявке (E.164).
WishesOverrideLeadWishes? (lead.md)НетWishesOverrideOverride хотелок или null.
Uniquenessstring (enum FixationUniqueness)ДаUniquenessNoDuplicates / HasDuplicates (результат авто-проверки).
DuplicateOfFixationIdGuid?НетDuplicateOfFixationIdЛучшее активное совпадение у застройщика.
DuplicateMatchKindstring? (enum DuplicateMatchKind)НетDuplicateMatchKindPhone / Name / PhoneAndName.
CreatedAtDateTimeOffsetДаCreatedAtМомент подачи.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент (permission crm.fixations.manage) в tenant своего агентства; либо Менеджер ПИН от имени агента (platform-scope).
  2. Проверка входа и предусловий.
    • формат полей: LeadId/DeveloperCompanyId != Guid.Empty, поля WishesOverride (диапазоны бюджета, enum строкой; заданные BudgetCurrencyId/CityId резолвятся в справочнике handbook (resolve-references.md): неизвестная/архивная валюта → ошибка валидации, неизвестный/архивный город → CRM_CITY_UNKNOWN) — иначе ошибка валидации;
    • лид-основание существует — иначе CRM_LEAD_NOT_FOUND;
    • лид принадлежит агентству актора и агент — активный участник лида (обычно Executor; или руководитель агентства / Менеджер ПИН), INV-FIX-2 — иначе CRM_LEAD_FORBIDDEN;
    • лид активен (Status = Active, INV-FIX-2 / INV-LEAD-2) — иначе CRM_LEAD_NOT_ACTIVE;
    • выбранный застройщик существует — иначе CRM_DEVELOPER_NOT_FOUND.
  3. Что читаем. Карточку клиента лида (обязательна) — источник snapshot ФИО/телефона. Активные фиксации выбранного застройщика — для проверки уникальности и авто-подсветки дублей.
  4. Бизнес-правила и дубль-детект (INV-FIX-001). Если по тройке (агент, инвестор, застройщик) уже есть активная фиксация (Status ∈ {Pending, FixedUnique, FixedNonUnique}) — отказ CRM_FIXATION_ACTIVE_EXISTS. Иначе свести дубль-детект по активным фиксациям того же застройщика: совпали телефон и ФИО → DuplicateMatchKind = PhoneAndName; только телефон → Phone; только ФИО → Name; лучшее совпадение → DuplicateOfFixationId, Uniqueness = HasDuplicates; иначе Uniqueness = NoDuplicates. Создать заявку Status = Pending, снять snapshot ФИО/телефона из карточки клиента, зафиксировать атрибуцию (OriginAgencyCompanyId/OriginAgentUserId — снимок агента-инициатора и его агентства на момент подачи) и текущего исполнителя (ExecutorAgentUserId, при создании = OriginAgentUserId), перенести из лида инвестора (InvestorUserId) и клиента, TenantId = DeveloperCompanyId, WishesOverride из тела. В ленту лида добавить событие LeadEventType.FixationCreated (payload LeadFixationRefPayload { FixationRequestId, DeveloperCompanyId }, INV-LEAD-2) и обновить LastEventAt.
  5. Что меняется. Появляется запись заявки на фиксацию и новое событие ленты лида — в одном согласованном изменении.
  6. События. Интеграционные Kafka-события на создании не публикуются — заявка ждёт решения застройщика; downstream-события возникают только при решении: fixation-approved.md (одобрение + авто-сделка) / fixation-rejected.md (отклонение).
  7. Результат. Карточка созданной заявки (включая результат дубль-детекта) → HTTP 201. При гонке уникальной тройки (INV-FIX-001) — CRM_FIXATION_ACTIVE_EXISTS.

Диаграмма

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

Предусловия. Актор — агент-владелец лида / руководитель агентства / Менеджер ПИН с crm.fixations.manage. Лид-основание существует, Status = Active, принадлежит агентству актора. Застройщик-адресат существует. У тройки (агент, инвестор, застройщик) нет активной фиксации (Status ∈ {Pending, FixedUnique, FixedNonUnique}, INV-FIX-001).

Ожидания от соседей (дубль-детект). Авто-подсветка дублей опирается на проекцию фиксаций Elasticsearch (fuzzy ФИО + точный телефон, ADR-0059); при её отставании/недоступности — деградация: Uniqueness может быть занижен до NoDuplicates, но заявка всё равно создаётся, а окончательное решение «уникальный/не уникальный» остаётся за менеджером застройщика при approve.

Постусловие (успех). Создана FixationRequestEntity: Status = Pending, TenantId = DeveloperCompanyId, иммутабельный snapshot ФИО/телефона из карточки клиента, зафиксирована атрибуция (OriginAgencyCompanyId/OriginAgentUserId — агент-инициатор и агентство на момент подачи), текущий исполнитель ExecutorAgentUserId (= OriginAgentUserId при создании), перенесены из лида инвестор (InvestorUserId) и клиент, WishesOverride из тела или null. Зафиксирован результат дубль-детекта (Uniqueness, DuplicateOfFixationId?, DuplicateMatchKind?). В ленте лида — событие FixationCreated, обновлён LastEventAt. Интеграционных событий нет — заявка ждёт решения застройщика и появляется в его списке (crm.fixations.decide) через проекцию (eventual). Инвестор о фиксации не уведомляется.

Постусловие (отказ). Ни заявки, ни события ленты лида.

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

ТипДетали
Интеграционные событияnone на создании (интеграционные — только на решении: fixation-approved.md, fixation-rejected.md)
Внешние вызовыпроверка существования застройщика (timeout 2 c, retry 1); batch-резолв BudgetCurrencyId/CityId из WishesOverride в справочнике handbook — только если заданы
Проекции / поискпроекция фиксаций (FixationRequestSearchDocument) обновляется проектором; дубль-детект — fuzzy-поиск по ФИО + точный телефон, ADR-0059
Уведомлениянет прямых; застройщик видит новую Pending-заявку в своём списке фиксаций (crm.fixations.decide)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат LeadId/DeveloperCompanyId/WishesOverride (включая неизвестный/архивный BudgetCurrencyId)«Проверьте корректность полей заявки»no retry
CRM_CITY_UNKNOWN422WishesOverride.CityId не найден или архивен в справочнике городов handbook«Город не найден в справочнике»no retry (выбрать город из lookup)
CRM_LEAD_NOT_FOUND404лид не найден«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403лид другого агентства/агента«Нет доступа к лиду»no retry
CRM_LEAD_NOT_ACTIVE409lead.Status != Active«Лид неактивен — создайте или продлите лид»no retry
CRM_DEVELOPER_NOT_FOUND404застройщик не найден«Застройщик не найден»no retry
CRM_FIXATION_ACTIVE_EXISTS409активная фиксация на тройку (Agent,Investor,Developer) уже есть (INV-FIX-001)«По этому клиенту уже есть активная фиксация у застройщика»no retry (при гонке — перечитать состояние)

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

  • WishesOverride и все поля FixationCardModel — additive optional без breaking change; удаление/ переименование/смена типа — breaking (migration + rollout).
  • Logs: fixation-request created (FixationRequestId, LeadId, DeveloperCompanyId, Uniqueness; ФИО/телефон — не пишутся); metrics: crm_fixation_requests_total{uniqueness}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path (без дублей). Given Active-лид и застройщик без совпадений; When POST; Then 201, заявка Pending, Uniqueness = NoDuplicates, в ленте лида FixationCreated.
  • Обнаружен дубль. Given у застройщика активная фиксация с тем же телефоном; When POST; Then 201, Uniqueness = HasDuplicates, DuplicateMatchKind = Phone, DuplicateOfFixationId заполнен.
  • Активная тройка (INV-FIX-001). Given активная фиксация той же тройки (агент, инвестор, застройщик); When POST; Then 409 CRM_FIXATION_ACTIVE_EXISTS.
  • Неактивный лид. Given lead.Status != Active; When POST; Then 409 CRM_LEAD_NOT_ACTIVE.
  • Чужой лид. Given лид другого агентства/агента; When POST; Then 403 CRM_LEAD_FORBIDDEN.
  • Деградация поиска. Given проекция Elasticsearch недоступна; When POST; Then 201, дубль-детект best-effort (возможно NoDuplicates), заявка создана.
  • PII. ФИО/телефон клиента не попадают в логи ни на одном исходе.

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

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

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