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

Entity / Aggregate: FixationRequestEntity

Дом полей агрегата «Заявка на фиксацию клиента» контекста crm (глоссарий «Заявка на фиксацию», «Фиксация»). Все проекции (списки фиксаций агента/агентства/застройщика, страница фиксации, payload pin.crm.fixation-approved.v1) ссылаются сюда и описывают только дельту.

Назначение

FixationRequestEntity — запрос агента застройщику закрепить клиента (ФИО + телефон) за агентом до сделки. Создаётся только на основе лида (lead.md); у одного лида может быть много одновременных фиксаций к одному и разным застройщикам. Заявка несёт snapshot ФИО/телефона клиента и опциональный override хотелок («шапка» остаётся в лиде; override — для UI/UX, если что-то поменялось, например срочность).

Застройщик при рассмотрении видит подсветку дублей по ФИО/телефону (полнотекстовый поиск) и решает: одобрить как уникальную (дублей нет) или не уникальную (клиент уже есть в базе застройщика), либо отклонить.

Одобрение — это авторизация, а не сделка

Застройщик подтверждает право агента вести этого клиента по своим объектам (пара «клиент × застройщик»), но сделку не создаёт. Одна одобренная фиксация авторизует агенту создать сколько угодно сделок по объектам этого застройщика (связь фиксация → сделки — 1:N, ведётся со стороны сделки, см. deal.md). Инвестор о фиксациях не знает.

Участники — без отдельной коллекции

В отличие от лида и сделки (единая модель ParticipantRole, ADR-0071), заявка на фиксацию проще и описывается тремя скалярными сторонами: иммутабельная атрибуция (OriginAgencyCompanyId/OriginAgentUserId)

  • текущий исполнитель (ExecutorAgentUserId, получатель комиссии) + инвестор (InvestorUserId). Смена исполнителя выполняется переназначением скаляра ExecutorAgentUserId (reassign), Origin при этом не трогается.

Классификация

ПолеЗначение
Контекстcrm
KindAggregateRoot
Source of truthcrm
Таблицаcrm.fixation_requests
Tenant isolationTenantId = DeveloperCompanyId (владелец решения по заявке — застройщик, ADR-0052); сторона агентства — иммутабельная атрибуция OriginAgencyCompanyId/OriginAgentUserId + текущий исполнитель ExecutorAgentUserId; видимость — по членству, а не по скаляру (ADR-0071)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор заявки на фиксацию.
TenantIdGuidДаindex ix_fixation_requests_tenant_idapp (выбранный застройщик)DeveloperCompanyId — застройщик-адресат.
OriginAgencyCompanyIdGuidДаindex ix_fixation_requests_origin_agency_company_idиз лида (active-agency claim на момент создания, ADR-0070)Агентство агента-инициатора на момент подачи: историческая атрибуция, воронка агентства, основание комиссии. Origin-снапшот, иммутабелен (ADR-0061 п.4, ADR-0071, INV-FIX-7) — не переписывается при смене агентом активного агентства.
OriginAgentUserIdGuidДаindex; часть ux_fixation_active_tripleиз лидаАгент-инициатор на момент подачи: историческая атрибуция и антифрод. Origin-снапшот, иммутабелен (ADR-0061 п.4, ADR-0071, INV-FIX-7) — не переписывается при передаче ведения/смене агентства.
ExecutorAgentUserIdGuidДаindex ix_fixation_requests_executor_agent_user_idпроизводное (текущий исполнитель)Текущий агент-исполнитель фиксации и получатель комиссии при одобрении. При создании совпадает с OriginAgentUserId; мутабельно — руководитель агентства/Менеджер ПИН может переназначить ведение (reassign) без переписывания Origin. Единственный источник истины исполнителя (ADR-0071); скаляр-«владелец» упразднён.
InvestorUserIdGuidДаindex; часть ux_fixation_active_tripleиз лидаИнвестор (клиент).
LeadIdGuidДаFK fk_fixation_requests_lead_id → crm.leads(id), indexappЛид-основание.
ClientIdGuidДаFK fk_fixation_requests_client_id → crm.clients(id), indexиз лидаКарточка клиента агентства.
StatusFixationStatusДаenum как text, varchar(32), index ix_fixation_requests_status; unique partial ux_fixation_active_triple on (OriginAgentUserId,InvestorUserId,TenantId) where status in ('Pending','FixedUnique','FixedNonUnique')appСм. enum и INV-FIX-001.
ClientNameFullName (owned)Даконтракт VO — client.mdsnapshot из ClientEntity на момент подачиФИО клиента в заявке (иммутабельный snapshot: застройщик видит то, что подал агент).
ClientPhoneNumberstringДаvarchar(32), E.164snapshot из ClientEntityТелефон клиента в заявке (ключ дубль-проверки).
WishesOverrideLeadWishes? (owned)Нетконтракт VO — lead.md, префикс колонок wishes_override_app (форма отправки фиксации)Override «шапки хотелок» для этой заявки; null — застройщику показываются актуальные Lead.Wishes через агентскую проекцию. Лид не изменяется.
UniquenessFixationUniquenessДаenum как text, varchar(32), default Unknownapp (авто-проверка) / решение застройщикаРезультат дубль-проверки (см. enum).
DuplicateOfFixationIdGuid?НетFK self fk_fixation_requests_duplicate_of → crm.fixation_requests(id)авто-проверкаАктивная фиксация-дубль у этого застройщика, на которую сработала подсветка (лучшее совпадение).
DuplicateMatchKindDuplicateMatchKind?Нетenum как text, varchar(32)авто-проверкаЧем совпало: телефон/ФИО/оба.
DecidedByUserIdGuid?Нетapp (менеджер застройщика)Кто одобрил/отклонил. null — решения ещё нет.
DecidedAtDateTimeOffset?НетappМомент решения.
RejectReasonstring?Нетvarchar(1000); обязателен при Status = Rejected (check ck_fixation_reject_reason)appПричина отклонения.
CancelledAtDateTimeOffset?Нетapp (агент)Момент отмены агентом.
CreatedAtDateTimeOffsetДаindex ix_fixation_requests_created_at descappПодача заявки.
CreatedByUserIdGuid?НетappАгент-инициатор (совпадает с OriginAgentUserId; Менеджер ПИН может подать от имени агента).
UpdatedAtDateTimeOffsetДаappДата последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (административно).
DeletedByUserIdGuid?НетappКто скрыл.

Enum FixationStatus («ожидает, зафиксирован, отклонён; зафиксирован уникальный / не уникальный»)

ЗначениеОписание
PendingОжидает решения застройщика.
FixedUniqueЗафиксирован как уникальный клиент (дублей в базе застройщика нет). Даёт право на комиссию.
FixedNonUniqueЗафиксирован как не уникальный (дубль есть; условия комиссии решает застройщик/rewards).
RejectedОтклонён застройщиком (RejectReason обязателен).
CancelledОтозван агентом до решения.

Enum FixationUniqueness

ЗначениеОписание
UnknownПроверка не выполнялась (черновой момент до сохранения; в БД практически не встречается).
NoDuplicatesАвто-проверка не нашла активных совпадений у застройщика.
HasDuplicatesНайдены совпадения; заполнены DuplicateOfFixationId/DuplicateMatchKind, UI подсвечивает дубль.

Enum DuplicateMatchKind

ЗначениеОписание
PhoneТочное совпадение нормализованного телефона.
NameFuzzy-совпадение ФИО (полнотекстовый поиск, порог score — конфигурация).
PhoneAndNameСовпали оба признака.

Дубль-детект (правило)

При создании заявки и при открытии карточки застройщиком выполняется проверка по активным фиксациям того же TenantId через поисковую проекцию FixationRequestSearchDocument (область поиска: TenantId = DeveloperCompanyId, IsActive = true; исключая текущую заявку):

  1. точное совпадение телефона — точный поиск по ClientPhoneNumberPhone;
  2. fuzzy-совпадение ФИО — нечёткий поиск по ClientName с порогом score (конфигурация) → Name;
  3. лучшее совпадение пишется в DuplicateOfFixationId + DuplicateMatchKind, Uniqueness = HasDuplicates; иначе NoDuplicates. Решение «уникальный/не уникальный» принимает менеджер застройщика — авто-проверка только подсвечивает («автоматически с проверкой дублей по номеру / ФИО»).

Value objects

  • FullName (owned, snapshot) — контракт в client.md; колонки client_name_*.
  • LeadWishes (owned, nullable override) — контракт в lead.md; колонки wishes_override_*
    • флаг наличия wishes_override_is_set boolean not null default false.

Модели JSONB (versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
LeadEntityreference (основание)LeadIdзагрузка при создании (проверка Status = Active)restrict
ClientEntityreferenceClientIdзагрузка при создании (snapshot ФИО/телефона)restrict
DealEntityinverse (1:N, авторизованные сделки)DealEntity.FixationRequestId → crm.fixation_requests(id)связь ведётся со стороны сделки; одна одобренная фиксация авторизует много сделок (deal.md)restrict (нельзя удалить фиксацию-основание при живых сделках)
FixationRequestEntity (дубль)self-referenceDuplicateOfFixationIdзагрузка при показе карточкиset null

Инварианты и переходы состояний

ИнвариантУсловиеКогда проверяетсяОшибка
INV-FIX-001На тройку (OriginAgentUserId,InvestorUserId,DeveloperCompanyId) — не более одной активной фиксации (Pending/FixedUnique/FixedNonUnique). Тройка строится на иммутабельном OriginAgentUserId (агент-инициатор), не на мутабельном исполнителе — переназначение ведения не создаёт/не ломает уникальностьпри создании заявки (unique partial ux_fixation_active_triple)CRM_FIXATION_ACTIVE_EXISTS
INV-FIX-2Создание — только из лида Status = Active, агентом-владельцем лида (или Менеджером ПИН)при создании заявкиCRM_LEAD_NOT_ACTIVE / CRM_LEAD_FORBIDDEN
INV-FIX-3Решение (Approve/Reject) — только из Pending, менеджером компании TenantIdпри решении по заявкеCRM_FIXATION_INVALID_STATE
INV-FIX-4Отмена (Cancel) — только из Pending, агентом-инициатором (или руководителем его агентства, Менеджером ПИН)при отмене заявкиCRM_FIXATION_INVALID_STATE / CRM_FIXATION_FORBIDDEN
INV-FIX-5Rejected требует RejectReason; FixedUnique допустим только при Uniqueness = NoDuplicates (иначе менеджер обязан выбрать FixedNonUnique или подтвердить уникальность вручную флагом команды forceUnique — фиксируется в audit)при решении по заявке (check ck_fixation_reject_reason)CRM_FIXATION_REASON_REQUIRED / CRM_FIXATION_DUPLICATE_CONFLICT
INV-FIX-6Одобрение — только авторизация: перевод в FixedUnique/FixedNonUnique подтверждает право агента вести клиента по объектам застройщика. Сделка при одобрении не создаётся; фиксация не хранит ссылку на сделку. Одна одобренная фиксация авторизует много сделок (пара «клиент × застройщик») — связь 1:N ведётся со стороны сделки (DealEntity.FixationRequestId, deal.md)при одобрении заявки
INV-FIX-7Иммутабельна только атрибуция OriginAgencyCompanyId/OriginAgentUserId — snapshot на момент подачи (set-once): не пересчитывается/не back-fill-ится при смене агентом агентства или передаче ведения (ADR-0061 п.4, ADR-0071). Переписывание исказило бы историческую воронку, антифрод и основание начислений rewards. Текущий исполнитель ExecutorAgentUserId мутабелен — может быть переназначен (reassign) руководителем агентства/Менеджером ПИН; при одобрении событие несёт актуального Executor (получателя комиссии)Origin — при создании (правки нет); ExecutorAgentUserId — при reassign (актор ∈ полномочные)CRM_FIXATION_SNAPSHOT_IMMUTABLE / CRM_FIXATION_REASSIGN_FORBIDDEN

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
FixationRequestedдоменноеСоздание заявкиId, TenantId, LeadId, OriginAgentUserId, ExecutorAgentUserId
FixationDecidedдоменноеApprove/Reject/CancelId, Status, DecidedByUserId
FixationApprovedEventинтеграционное, topic pin.crm.fixation-approved.v1, ContractVersion = 1Pending → FixedUnique/FixedNonUniqueFixationRequestId, TenantId (застройщик), OriginAgencyCompanyId, OriginAgentUserId, ExecutorAgentUserId, CommissionRecipientUserId (= ExecutorAgentUserId на момент одобрения — получатель комиссии, ADR-0071), InvestorUserId, Status, DecidedAt (сделки нет — событие авторизации, DealId не передаётся); консюмеры: rewards (право/eligibility исполнителя; комиссия — при завершении сделки), notifications; контракт — ../events/fixation-approved.md
FixationRejectedEventинтеграционное, topic pin.crm.fixation-rejected.v1, ContractVersion = 1Pending → RejectedFixationRequestId, TenantId, OriginAgentUserId, ExecutorAgentUserId, RejectReason, DecidedAt; консюмер: notifications; контракт — ../events/fixation-rejected.md

Интеграционные события публикуются атомарно с переходом статуса.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_fixation_requests_tenant_id; ix_fixation_requests_origin_agency_company_id; btree (origin_agent_user_id), ix_fixation_requests_executor_agent_user_id, (investor_user_id), (lead_id), (client_id); ux_fixation_active_triple unique partial (origin_agent_user_id,investor_user_id,tenant_id); ix_fixation_requests_status; ix_fixation_requests_created_at desc
Полнотекстовый поискElasticsearch-проекция FixationRequestSearchDocument (../search/fixations.md, ADR-0059) — поиск списков агента/застройщика + дубль-детект (fuzzy ФИО + точный телефон); PostgreSQL-FTS (tsvector/GIN) не используется
Партиционирование / retentionnone (объём мал; история хранится бессрочно как основание комиссий)
Concurrencyоптимистическая блокировка: одновременные Approve/Cancel — проигравший получает конфликт и перечитывает состояние
Permissions / PIIcrm.fixations.view / crm.fixations.manage (агентская сторона), crm.fixations.decide (застройщик). Видимость по членству/полномочию (ADR-0071), не по скаляру-владельцу: доступ имеет актор, который является текущим исполнителем (ExecutorAgentUserId) или админ/руководитель компании-владельца агентской стороны (по OriginAgencyCompanyId, в рамках scope-иерархии ADR-0025) или Менеджер ПИН (platform-scope). Сторона застройщика — заявки своего TenantId; инвестор доступа не имеет. PII: ClientName/ClientPhoneNumber — маскирование телефона в списках застройщика до решения не требуется (данные переданы для фиксации осознанно), в логи — только Id.
Auditотметки создания/изменения/удаления (кто и когда); решения (DecidedByUserId,DecidedAt,RejectReason) — иммутабельные факты

Межсервисные последовательности и промежуточные состояния

Дубль-детект и поисковая реплика. Проверка дублей выполняется по поисковой проекции FixationRequestSearchDocument (Elasticsearch), которая наполняется асинхронно из изменений фиксаций. Проекция — eventually consistent: только что поданная соседняя заявка может не попасть в индекс мгновенно, поэтому авто-подсветка HasDuplicates — вспомогательная, а решение уникальности всегда за менеджером застройщика (INV-FIX-5). Дубль-детект запускается дважды: при подаче заявки (первичная отметка Uniqueness) и при открытии карточки застройщиком (пересчёт по свежему индексу).

Одобрение (авторизация, без сделки). В одном согласованном изменении: заявка → FixedUnique/FixedNonUnique, проставляются DecidedByUserId/DecidedAt (INV-FIX-6) — сделка не создаётся, фиксация ссылки на сделку не хранит. Далее асинхронно публикуется FixationApprovedEvent (→ rewards: право/eligibility текущего исполнителя по CommissionRecipientUserId, основание — по иммутабельному Origin; комиссия материализуется позже — при завершении сделки; notifications — агенту «фиксация одобрена»). Инвестор о факте фиксации не уведомляется (о фиксациях не знает). Сделки по объектам застройщика агент создаёт отдельно — одобренная фиксация авторизует их сколько угодно (связь 1:N со стороны сделки, deal.md).

  • Что видит агент/застройщик. Заявка сразу переходит в одобренный статус (данные crm); в списке застройщика — FixedUnique/FixedNonUnique. Право (eligibility) в rewards — с задержкой (eventual); сделок в этот момент нет.

Отклонение/отмена. → RejectedFixationRejectedEvent (→ notifications агенту с причиной); → Cancelled (агентом) закрывает активную тройку без внешних эффектов, кроме снятия блокировки INV-FIX-001.

Отказы, идемпотентность и конкуренция

  • Единственность активной тройки. Структурная защита — unique partial ux_fixation_active_triple на (AgentUserId,InvestorUserId,DeveloperCompanyId); повтор подачи в активной фазе → CRM_FIXATION_ACTIVE_EXISTS. Гонка двух одновременных подач на одну тройку → ровно одна Pending.
  • Гонка «одобрение ↔ отмена». Approve менеджера и Cancel агента конкурируют за Pending; оптимистическая блокировка по версии: побеждает первый коммит, проигравший перечитывает состояние и видит CRM_FIXATION_INVALID_STATE (заявка уже не Pending). Если одобрение выиграло — отмена невозможна (заявка уже решена); если отмена выиграла — менеджер видит Cancelled.
  • Устаревший дубль-детект. Если между подсветкой и решением индекс изменился (появился/ушёл дубль), менеджер всё равно решает вручную; при Uniqueness = HasDuplicates выбор FixedUnique требует явного флага forceUnique (фиксируется в audit) — иначе CRM_FIXATION_DUPLICATE_CONFLICT (INV-FIX-5).
  • Идемпотентность одобрения. Переход возможен только из Pending — повтор команды одобрения над уже решённой заявкой отклоняется (CRM_FIXATION_INVALID_STATE); FixationApprovedEvent публикуется ровно один раз (при первом одобрении). Сделка при одобрении не создаётся, поэтому двойной сделки быть не может.
  • Иммутабельная атрибуция vs мутабельный исполнитель. OriginAgencyCompanyId/OriginAgentUserId — set-once (INV-FIX-7); смена агентом агентства или передача ведения после подачи не переписывают Origin (основание/атрибуцию rewards). Текущий исполнитель ExecutorAgentUserId может быть переназначен (reassign); при одобрении получатель комиссии (CommissionRecipientUserId) снимается с актуального ExecutorAgentUserId на момент решения.

Acceptance-критерии

  • Happy (одобрение уникального). Дано: заявка Pending, Uniqueness = NoDuplicates. Когда менеджер застройщика одобряет как уникальную. Тогда заявка FixedUnique, сделка не создаётся (фиксация ссылки на сделку не хранит), опубликован FixationApprovedEvent без DealId с CommissionRecipientUserId = ExecutorAgentUserId (→ rewards фиксирует право/eligibility текущего исполнителя; комиссия — при завершении будущей сделки).
  • Переназначение исполнителя перед одобрением. Дано: заявка Pending, руководитель агентства сменил ExecutorAgentUserId на другого агента (reassign). Когда застройщик одобряет. Тогда Origin* не изменён, FixationApprovedEvent.CommissionRecipientUserId равен новому ExecutorAgentUserId.
  • Дубль при подаче. Дано: активная фиксация с тем же телефоном у застройщика. Когда агент открывает карточку. Тогда Uniqueness = HasDuplicates, заполнены DuplicateOfFixationId/DuplicateMatchKind.
  • Уникальный при наличии дубля. Дано: Uniqueness = HasDuplicates. Когда менеджер выбирает FixedUnique без forceUnique. Тогда отказ CRM_FIXATION_DUPLICATE_CONFLICT.
  • Отклонение без причины. Дано: заявка Pending. Когда менеджер отклоняет без RejectReason. Тогда отказ CRM_FIXATION_REASON_REQUIRED.
  • Повторная активная тройка. Дано: активная фиксация на (агент, инвестор, застройщик). Когда подаётся вторая. Тогда отказ CRM_FIXATION_ACTIVE_EXISTS.
  • Лид не активен. Дано: лид Expired/Archived. Когда агент подаёт фиксацию. Тогда CRM_LEAD_NOT_ACTIVE.
  • Гонка одобрение/отмена. Дано: Pending, менеджер одобряет и агент отменяет одновременно. Когда оба выполняются. Тогда применяется один переход; проигравший получает CRM_FIXATION_INVALID_STATE.

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

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

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

  • lead.md (основание, VO LeadWishes), client.md (VO FullName), deal.md (сделки, авторизованные фиксацией; связь 1:N через DealEntity.FixationRequestId, состав ParticipantRole).
  • Комиссия по фиксации — docs/06-services/rewards/README.md (будущий путь по конвенции); получатель — CommissionRecipientUserId события одобрения (= текущий Executor), основание — по Origin.
  • ADR-0052 (tenancy двухсторонних сущностей); ADR-0070 (мульти-агентство: AgencyCompanyId из active-agency claim на момент создания); ADR-0071 (Origin-атрибуция ⟂ Executor, видимость по членству, CommissionRecipientUserId); ADR-0061 п.4 (запрет back-fill Origin). Поиск списков и дубль-детект — Elasticsearch-проекция ../search/fixations.md (ADR-0059), PostgreSQL-FTS не используется.