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

GET /api/developer/crm/fixations — Список фиксаций застройщика (+ FTS)

Список заявок на фиксацию со стороны застройщика: менеджер видит только заявки, адресованные его компании, с фильтрами по статусу/агентству и полнотекстовым поиском по ФИО+телефон, с подсветкой дублей. Read-only проекция агрегата FixationRequestEntity.

Назначение

Отдать менеджеру застройщика постранично заявки на фиксацию своего TenantId для экрана «Фиксации» ЛК застройщика — вход в принятие решения (одобрить уникальный/не уникальный / отклонить, approve/reject). В отличие от агентской стороны (list-fixations.md), здесь FixationRequestEntity.TenantId = DeveloperCompanyId совпадает со скоупом актора — разграничение по tenant применяется автоматически, без scope-bypass (ADR-0052). Каждая строка несёт результат авто-проверки дублей (Uniqueness, ссылка на дубль-фиксацию, тип совпадения) для подсветки в UI.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupfixations
Статусdraft
Документdocs/06-services/crm/api/developer/list-fixations.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика)
Auth policy / Permissionscrm.fixations.decide (identity RBAC); инвестор доступа не имеет
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelне применяется: застройщик видит все заявки своего TenantId (клиентскую базу/лиды агентства — нет). OriginAgencyCompanyId/OriginAgentUserId — опциональные фильтры, не ограничение
Tenant isolationTenantId в каждом запросе (tenant_id = DeveloperCompanyId) + отсечение soft-deleted; применяются автоматически
Auditread (не sensitive: данные переданы застройщику для фиксации осознанно); в логи — только Id/счётчики, телефон/ФИО не пишутся
Доступ

FixationRequestEntity.TenantId = DeveloperCompanyId совпадает со scope актора — в отличие от агентской проекции того же дома (list-fixations.md), где TenantId заявки не совпадает со scope актора и требуется явный record-level предикат. Здесь разграничение по tenant применяется автоматически, без scope-bypass (ADR-0052); поверх tenant-скоупа record-level не применяется — застройщик видит все заявки своего TenantId (клиентскую базу/лиды агентства — нет).

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/developer/crm/fixations
Success status200
Idempotency headerне применяется (безопасный read)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired, != Guid.Empty (DeveloperCompanyId)Tenant isolation
OriginAgencyCompanyIdqueryGuid?Нет!= Guid.Empty если задан; несуществующий/чужой → пустая страница, без 404Фильтр по агентству-инициатору (историческая атрибуция)
OriginAgentUserIdqueryGuid?Нет!= Guid.Empty если заданФильтр по агенту-инициатору (историческая атрибуция)
StatusqueryFixationStatus?Нетenum строкой; неизвестное значение → ValidationError
UniquenessqueryFixationUniqueness?Нетenum строкой; быстрый фильтр «только дубли» (HasDuplicates) для рассмотрения
SearchTermquerystring?Нетtrim, 1..128 после trim; передаётся как SearchQuery multi-match (fuzzy), не raw-DSLPII (ФИО/телефон): в логи не пишется
PagingqueryPagedQueryНетTake ∈ [1..200] (default 20), Offset >= 0 (default 0)
SortquerySortQuery<FixationSortField>?НетFixationSortField ∈ {CreatedAt, DecidedAt, Status}; default CreatedAt desc (при поиске — relevance/score desc)

Все фильтры комбинируются по AND; Status/Uniqueness/OriginAgencyCompanyId/OriginAgentUserId — логические предикаты (отсутствие совпадений → пустая страница, не ошибка). Сортировка типизирована SortQuery<FixationSortField> (enum, не строка), tiebreak Id desc.

Модель ответа — страница DeveloperFixationListItemModel

Постраничный контейнер: Items (список DeveloperFixationListItemModel) + Meta (Total, Take, Offset, HasMore). Поля строки — проекция FixationRequestEntity (здесь только дельта списка):

DeveloperFixationListItemModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаFixationRequestEntity.IdИдентификатор заявки (ключ перехода на карточку рассмотрения).
ClientNameFullNameViewДаClientName (VO FullName, snapshot)ФИО клиента; контракт VO — client.md.
ClientPhoneNumberstringДаClientPhoneNumber (snapshot, E.164)+7***NNNN только в логахТелефон клиента (ключ дубль-проверки; передан застройщику осознанно — в ответе не маскируется).
OriginAgencyCompanyIdGuidДаOriginAgencyCompanyIdАгентство-инициатор (историческая атрибуция); display-имя резолвит фронт/identity (не в проекции).
OriginAgentUserIdGuidДаOriginAgentUserIdАгент-инициатор заявки (историческая атрибуция).
StatusFixationStatusДаStatusenum строкой: Pending/FixedUnique/FixedNonUnique/Rejected/Cancelled (дом).
UniquenessFixationUniquenessДаUniquenessenum строкой: Unknown/NoDuplicates/HasDuplicates (результат авто-проверки дублей).
DuplicateOfFixationIdGuid?НетDuplicateOfFixationIdАктивная фиксация-дубль того же TenantId (лучшее совпадение); null при NoDuplicates. Карточку резолвит фронт (self-ссылка, дом).
DuplicateMatchKindDuplicateMatchKind?НетDuplicateMatchKindenum строкой: Phone/Name/PhoneAndName — чем сработала подсветка; null при NoDuplicates.
CreatedAtDateTimeOffsetДаCreatedAtПодача заявки (ключ сортировки по умолчанию).

Все enum — строкой; дом значений и правило дубль-детекта — fixation-request.md.

Алгоритм

Операция только читает — шагов записи, событий и транзакции нет.

  1. Контекст и доступ. Выполняет менеджер застройщика (User) в своём tenant (TenantId = DeveloperCompanyId); требуется permission crm.fixations.decide, иначе CRM_FIXATIONS_FORBIDDEN. Параметры — пагинация, сортировка, фильтры и поисковая строка.
  2. Проверка входа. Paging.Take ∈ 1..200, Offset >= 0; SearchTerm (если задан) — 1..128 после trim; Status/Uniqueness/Sort.Field — известные значения enum (unknown → VALIDATION_FAILED, не «тихий» фильтр); OriginAgencyCompanyId/OriginAgentUserId != Guid.Empty при заданных. Существование по id не проверяется — это логические фильтры (несуществующий/чужой id → пустая страница, без 404). Scope — только tenant; record-level не применяется.
  3. Что читаем. Заявки на фиксацию застройщика с опциональными фильтрами (AND): OriginAgencyCompanyId, OriginAgentUserId, Status, Uniqueness. При заданном SearchTerm — предварительный отбор FixationRequestId через поисковую проекцию FixationRequestSearchDocument (multi-match по ФИО+телефон, fuzzy; scope = TenantId застройщика, ADR-0059), затем предикат Id ∈ ids, сортировка по релевантности desc; иначе сортировка из Sort (default CreatedAt desc → Id desc). Разграничение по TenantId = DeveloperCompanyId и отсечение soft-deleted применяются автоматически (bypass не нужен, в отличие от агентской стороны).
  4. Бизнес-правила и маппинг. Переходов состояний нет. Маппинг поле-в-поле — таблица DeveloperFixationListItemModel; DuplicateOfFixationId/DuplicateMatchKind/Uniqueness берутся из уже сохранённого результата авто-проверки (правило детекта — fixation-request.md, «Дубль-детект»), в списке не пересчитываются. Enum — строкой.
  5. Результат. Страница DeveloperFixationListItemModel (Items + Meta { Total, Take, Offset, HasMore }, Total — счётчик того же предиката) → HTTP 200. Пустой результат — 200 с Items = [] (не 404). PII: ClientPhoneNumber — в ответе без маски, в логи — только Id/Total.

Диаграмма

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

ТипДетали
Integration eventsnone (read-only)
External callsnone (display-имена агентств/агентов и карточка дубль-фиксации резолвятся отдельно на фронте)
Cache / projections / searchпри поиске — чтение поисковой проекции FixationRequestSearchDocument (multi-match, scope по tenant, ADR-0059); PostgreSQL-индексы фильтров: ix_fixation_requests_tenant_id, ix_fixation_requests_origin_agency_company_id, ix_fixation_requests_status, ix_fixation_requests_created_at
Notificationsnone

Ошибки и failure modes

Ошибки

Несуществующие/чужие значения OriginAgencyCompanyId/OriginAgentUserId/Status/Uniqueness не считаются ошибкой — это логические фильтры, при отсутствии совпадений GET отдаёт 200 с пустой страницей, без 404. Ошибкой являются только неизвестные значения enum и нарушения формата входных параметров (см. таблицу ниже).

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Take/Offset вне диапазона; неизвестный Status/Uniqueness/Sort.Field; SearchTerm > 128; Guid.Empty в фильтре«Проверьте параметры запроса»no retry
CRM_FIXATIONS_FORBIDDEN403нет permission crm.fixations.decide / нет DeveloperCompanyId в claims«Недостаточно прав для просмотра фиксаций»no retry
UNAUTHORIZED401нет/невалидный JWT«Требуется авторизация»no retry

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

  • Новые optional фильтры/поля ответа — additive (без breaking). Удаление/переименование полей или сужение enum FixationStatus/FixationUniqueness/DuplicateMatchKind — breaking (миграция клиента + rollout); неизвестный член enum на клиенте — по правилу unknown-enum (cross-cutting «Совместимость»).
  • Logs: developer-fixations listed (TenantId, набор фильтров, Total, флаг fts; телефон/ФИО — не пишутся); metrics: crm_developer_fixations_list_total{fts,has_duplicates}, latency p95 (< 1 c, SLO списков); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

Acceptance criteria

  • Given заявки на фиксацию, адресованные застройщику, When менеджер запрашивает список без фильтров, Then 200 со страницей DeveloperFixationListItemModel (CreatedAt desc → Id desc), где каждая строка несёт результат авто-проверки дублей (Uniqueness, DuplicateOfFixationId, DuplicateMatchKind).
  • Given фильтр Uniqueness = HasDuplicates, When GET, Then в выдаче только заявки с найденными дублями (для приоритетного рассмотрения).
  • Given SearchTerm длиной 1..128, When GET, Then предварительный отбор через FixationRequestSearchDocument (multi-match по ФИО + телефон, fuzzy, scope = TenantId застройщика), затем фильтры; сортировка — по релевантности.
  • Given неизвестное значение Status/Uniqueness/Sort.Field, When GET, Then 400 VALIDATION_FAILED; OriginAgencyCompanyId/OriginAgentUserId несуществующий → 200 пустая страница (это фильтр).
  • Given результат отдан, When проверяется PII, Then ClientPhoneNumber в ответе без маски (данные переданы застройщику осознанно), а в логи — только Id/Total (телефон/ФИО не пишутся).
  • Given актор без crm.fixations.decide или без DeveloperCompanyId в claims, When GET, Then 403 CRM_FIXATIONS_FORBIDDEN; без JWT — 401.

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

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

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