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

FixationRequestSearchDocument

Поисковая проекция заявок на фиксацию FixationRequestSearchDocument: обслуживает поиск в списках агента/агентства/застройщика (../api/agent/list-fixations.md, ../api/developer/list-fixations.md) и дубль-детект по ФИО/телефону при подаче заявки и открытии карточки застройщиком. Проекция перестраиваемая — её всегда можно восстановить из источника истины.

Назначение

Источник истины — сущность FixationRequestEntity в PostgreSQL (crm.fixation_requests); проекция хранит индексируемые/фильтруемые/сортируемые поля + иммутабельный snapshot ФИО/телефона клиента (застройщик видит то, что подал агент). Прямого обращения к поисковому индексу из бизнес-логики нет — она работает с проекцией через типизированный поисковый запрос (ADR-0059).

Index

ПолеЗначение
Index aliascrm-fixations
Versioned indexcrm-fixations-v1 (переключение alias — ADR-0029)
Owner servicecrm
Source of truthFixationRequestEntity (../domain/fixation-request.md) в PostgreSQL
Rebuild jobпериодический/ручной ребилд FixationRequestSearchRebuildJob

Наполнение — инкрементально по доменным событиям агрегата плюс периодический/ручной rebuild для bootstrap и восстановления консистентности (ADR-0059).

Document Id И Routing

  • Document id: <TenantId>:<FixationRequestId> (TenantId = DeveloperCompanyId, ADR-0052) — детерминирован, upsert идемпотентен.
  • Routing key: TenantId (DeveloperCompanyId) — заявки одного застройщика колокализуются; дубль-детект ограничен одним застройщиком, routing совпадает со scope.
  • Delete/tombstone: soft-delete (DeletedAt != null, административное сокрытие) → удаление документа. Финалы (Rejected/Cancelled) документ не удаляют (история ищется), лишь меняют фильтруемое Status и выводят заявку из активного множества дубль-детекта (IsActive).

Поля

ПолеТипIndexedStoredAnalyzerSourceSecurity
DocumentIdkeywordдадаTenantId:FixationRequestIdinclude
FixationRequestIdkeywordдадаFixationRequestEntity.Idinclude
TenantIdkeywordданетFixationRequestEntity.TenantId (DeveloperCompanyId)scope-only (не выдаётся)
OriginAgencyCompanyIdkeywordдадаFixationRequestEntity.OriginAgencyCompanyIdrecord-level scope (агентство-атрибуция)
ExecutorAgentUserIdkeywordдадаFixationRequestEntity.ExecutorAgentUserIdrecord-level scope (текущий исполнитель) + фильтр
InvestorUserIdkeywordдадаFixationRequestEntity.InvestorUserIdinclude
LeadIdkeywordдадаFixationRequestEntity.LeadIdinclude
ClientIdkeywordдадаFixationRequestEntity.ClientIdinclude
StatuskeywordдадаFixationRequestEntity.Status (FixationStatus)фильтр
IsActivebooleanдадаStatus ∈ {Pending, FixedUnique, FixedNonUnique}фильтр дубль-детекта
UniquenesskeywordдадаFixationRequestEntity.Uniquenessinclude
ClientNametextдадаru_translit_fuzzyFixationRequestEntity.ClientName (snapshot FullName)PII (маска в логах)
ClientPhoneNumberkeyword + textдадаdigits_edge_ngram (text-подполе)FixationRequestEntity.ClientPhoneNumber (E.164, snapshot)PII (ключ дубль-детекта; маска в логах)
SearchTexttextданетru_translit_fuzzyClientName ⊕ ClientPhoneNumber (copy_to)основной матч поиска
CreatedAtdateдадаFixationRequestEntity.CreatedAtфильтр-диапазон + сортировка (default desc)
DecidedAtdateдадаFixationRequestEntity.DecidedAtсортировка
SchemaVersionintegerдадаверсия схемы проекцииверсия схемы проекции
UpdatedAtdateдадаFixationRequestEntity.UpdatedAtfreshness watermark
IndexedAtdateдадаиндексаторfreshness

Внешний ввод — только поисковая строка (multi-match по SearchText с fuzziness), не raw-DSL. Фильтры (Status/ExecutorAgentUserId/OriginAgencyCompanyId/диапазон CreatedAt) и сортировки (CreatedAt/DecidedAt/Status, relevance при поиске) задаются типизированными параметрами запроса.

Дубль-детект

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

  1. Точное совпадение телефона — exact term по ClientPhoneNumber (keyword) → Phone.
  2. Fuzzy-совпадение ФИО — multi-match по SearchText/ClientName с fuzziness; порог score — конфигурация → Name.
  3. Лучшее совпадение пишется в DuplicateOfFixationId + DuplicateMatchKind (Phone/Name/PhoneAndName), Uniqueness = HasDuplicates; иначе NoDuplicates.
Кто решает

Решение «уникальный/не уникальный» принимает менеджер застройщика — авто-проверка только подсвечивает совпадение, но не блокирует и не решает за него.

Source Events

EventVersionTransform
FixationRequested1upsert документа из состояния FixationRequestEntity (ФИО/телефон snapshot, IsActive = true)
FixationDecided1partial-upsert Status/Uniqueness/DecidedAt; пересчёт IsActive
soft-delete (DeletedAt)1delete документа по DocumentId

ФИО/телефон в проекции — иммутабельный snapshot (не обновляются событием identity: застройщик видит то, что подал агент). Наполняется по доменным событиям агрегата + периодический/ручной rebuild для bootstrap и восстановления консистентности из PostgreSQL.

Устойчивость применения

FixationDecided может прийти повторно/с перестановкой — partial-upsert применяется монотонно по UpdatedAt, upsert по детерминированному DocumentId идемпотентен; пересчёт IsActive детерминирован от Status, поэтому повтор не меняет активное множество дубль-детекта. Неизвестный тип события пропускается (лог, без остановки индексатора).

Freshness

  • SLA: search (near-real-time, < 1 мин; eventual относительно PostgreSQL, FreshnessView — ADR-0019).
  • Watermark: IndexedAt + UpdatedAt заявки в документе.
  • Stale behavior: дубль-детект подсвечивает, не блокирует; финальное решение менеджер принимает по карточке (../api/developer/approve-fixation.md), читающей source-of-truth PostgreSQL; уникальность триплета гарантирует уникальный индекс активной тройки ux_fixation_active_triple (INV-FIX-001).
Гонка почти одновременных заявок

Окно свежести: проекция near-real-time (< 1 мин), поэтому две почти одновременные заявки на один триплет могут обе не увидеть друг друга в авто-проверке (обе ещё не проиндексированы). Это допустимо — жёсткую единственность активной тройки (агент, инвестор, объект) держит ux_fixation_active_triple (INV-FIX-001) на источнике истины: проигравшая заявка отклоняется независимо от свежести проекции.

Security

  • Scope-фильтр применяется до выполнения запроса (ADR-0059, ADR-0014) — record-level: агент видит заявки, где он текущий исполнитель (ExecutorAgentUserId = actor); руководитель/админ агентства — все заявки своего OriginAgencyCompanyId c фильтром по агенту; застройщик — только заявки своего TenantId (DeveloperCompanyId); инвестор доступа не имеет; Менеджер ПИН — platform-scope. Дубль-детект — всегда в scope одного TenantId.
  • Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные; TenantId — только scope-фильтр.
PII-поля

ClientName, ClientPhoneNumber индексируются для поиска/дубль-детекта; маскирование в списках застройщика до решения не требуется — данные переданы для фиксации осознанно; в логах — маска.

Rebuild

  1. Создать версионный индекс crm-fixations-vN (маппинг + ru_translit_fuzzy/digits_edge_ngram).
  2. Backfill из PostgreSQL: живые заявки (DeletedAt == null) стримом по FixationRequestEntity, map в FixationRequestSearchDocument (IsActive из Status).
  3. Валидация counts/checksums против источника (crm.fixation_requests).
  4. Атомарно переключить alias crm-fixations на новую версию.
  5. Держать предыдущий индекс до конца rollback-окна.

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

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

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