GET /api/agent/crm/fixations — Список фиксаций агентства (+ поиск)
Список заявок на фиксацию со стороны агентства: агент видит свои, руководитель агентства — все по своей компании, с фильтрами агент/застройщик/статус и полнотекстовым поиском по ФИО+телефон. Дом полей — fixation-request.md.
Назначение
Отдать агенту/руководителю агентства страницу фиксаций его агентства для экрана «Мои фиксации». Ключевая
особенность tenancy: заявка TenantId = DeveloperCompanyId (владелец решения — застройщик, ADR-0052), а актор —
агентская сторона. Поэтому выборка идёт не по автоматическому tenant-scope, а по явному record-level
предикату OriginAgencyCompanyId = actor.CompanyId (см. Актор и доступ и
fixation-request.md → Безопасность).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent (JWT агента/руководителя агентства; permission crm.fixations.view) |
| Feature group | fixations |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/list-fixations.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент или Руководитель агентства) |
| Auth policy / Permissions | crm.fixations.view; инвестор доступа не имеет |
| Scope (RBAC) | организация (агентство) — actor.CompanyId = OriginAgencyCompanyId |
| Record-level | Агент — только свои (ExecutorAgentUserId = actor.UserId, текущий исполнитель); Руководитель агентства — все по OriginAgencyCompanyId с опциональным фильтром по исполнителю |
| Tenant isolation | Особый случай: TenantId заявки = DeveloperCompanyId ≠ scope актора. Автоматический tenant-фильтр отключается; изоляция гарантируется обязательным предикатом OriginAgencyCompanyId = actor.CompanyId + DeletedAt is null. Кросс-агентский доступ конструктивно невозможен (значение берётся из claims, не из запроса) |
| Audit | read (не sensitive: агентство запрашивает собственные данные); в логи — только Id/счётчики, телефон маскируется |
Особый случай tenancy: TenantId заявки = DeveloperCompanyId (владелец решения — застройщик), а не
OriginAgencyCompanyId актора. Автоматический tenant-фильтр по этой причине отключается — изоляцию агентства
гарантирует обязательный предикат OriginAgencyCompanyId = actor.CompanyId (значение берётся из claims, не из
запроса) + DeletedAt is null. Кросс-агентский доступ конструктивно невозможен.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/agent/crm/fixations |
| Success status | 200 |
| Idempotency header | не применяется (безопасный read) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | Required | Record-level (агент — свои по ExecutorAgentUserId) |
ActorCompanyId | identity claims | Guid | Да | Required, != Guid.Empty | агентство актора; сверяется с OriginAgencyCompanyId заявки — обязательный предикат выборки |
IsAgencyManager | identity claims (роль) | bool | Да | из роли/permission руководителя | Определяет record-level (свои vs всё агентство) |
ExecutorAgentUserId | query | Guid? | Нет | != Guid.Empty если задан; для агента разрешён только = ActorUserId (иначе CRM_FIXATIONS_FORBIDDEN) | Фильтр руководителя по текущему исполнителю |
DeveloperCompanyId | query | Guid? | Нет | != Guid.Empty если задан | Фильтр по застройщику (= TenantId заявки) |
Status | query | FixationStatus? | Нет | enum строкой; неизвестное значение → ValidationError | — |
SearchTerm | query | string? | Нет | trim, 1..128 после trim; поисковый запрос multi-match (fuzzy), не raw-DSL | PII (телефон/ФИО): в логи не пишется |
Paging | query | PagedQuery | Нет | Take ∈ [1..200] (default 20), Offset >= 0 | — |
Sort | query | SortQuery<FixationSortField>? | Нет | FixationSortField ∈ {CreatedAt, DecidedAt, Status}; default CreatedAt desc (при поиске — relevance desc) | — |
Модель ответа — постраничный список FixationListItemModel
Постраничный контейнер: Items: IReadOnlyList<FixationListItemModel>, Meta: {Total, Take, Offset, HasMore}.
FixationListItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | FixationRequestEntity.Id | — | Идентификатор заявки (ключ перехода на карточку). |
ClientName | FullNameView | Да | ClientName (VO FullName, snapshot) | — | ФИО клиента; контракт VO — client.md. |
ClientPhoneNumber | string | Да | ClientPhoneNumber (snapshot, E.164) | +7***NNNN только в логах | Телефон клиента (собственные данные агентства — в ответе не маскируется). |
DeveloperCompanyId | Guid | Да | TenantId | — | Застройщик-адресат; display-имя резолвится фронтом/identity. |
Status | FixationStatus | Да | Status | — | enum строкой: Pending / FixedUnique / FixedNonUnique / Rejected / Cancelled (дом). |
Uniqueness | FixationUniqueness | Да | Uniqueness | — | enum строкой: Unknown / NoDuplicates / HasDuplicates (результат дубль-проверки). |
DecidedAt | DateTimeOffset? | Нет | DecidedAt | — | Момент решения застройщика; null — ещё Pending/Cancelled. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Подача заявки (ключ сортировки по умолчанию). |
Алгоритм
- Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission
crm.fixations.view); якорь —ActorCompanyId(AgencyCompanyId); признакIsAgencyManagerопределяет охват (свои vs всё агентство). - Проверка входа и предусловий.
- формат:
Paging.Take ∈ [1..200],Offset >= 0;SearchTerm(если задан) —1..128после trim;Status— известное значение enum;ExecutorAgentUserId/DeveloperCompanyId—!= Guid.Empty, если заданы — иначе ошибка валидации; - scope:
ActorCompanyId != Guid.Empty(иначеCRM_FIXATIONS_FORBIDDEN); если!IsAgencyManagerиExecutorAgentUserId != ActorUserId→CRM_FIXATIONS_FORBIDDEN(агент не смотрит чужие фиксации); - несуществующие фильтры дают пустую страницу, не ошибку.
- формат:
- Что читаем. Выборка со scope-фильтром по агентству (авто-tenant-фильтр отключён; иначе фильтр
tenant_id = DeveloperCompanyIdне соответствовал бы скоупу агентства). При заданномSearchTerm— предварительный отбор идентификаторов через поисковую проекциюFixationRequestSearchDocument(multi-match по ФИО+телефон, fuzzy; scope-фильтр = agency record-level, ADR-0059). - Бизнес-правила и построение предиката. Обязательная база (никогда не опускается):
OriginAgencyCompanyId == ActorCompanyIdиDeletedAt == null. Record-level: если не руководитель — добавитьExecutorAgentUserId == ActorUserId; иначе — опциональныйExecutorAgentUserId == query.ExecutorAgentUserId. Опциональные фильтры:TenantId == DeveloperCompanyId,Status == query.Status. Поиск: при заданномSearchTerm— ограничение по найденным id, сортировка по relevance desc; иначе —Sort(defaultCreatedAtdesc). Маппинг поле-в-поле — по таблице выше. - Результат. Постраничный список (
Items— проекция страницы,Meta—Totalпо тому же предикату +Take/Offset) → HTTP200. PII:ClientPhoneNumberотдаётся владеющему агентству без маски; в логи — толькоId/Total.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (read-only) |
| Внешние вызовы | none (display-имена застройщиков/агентов резолвятся отдельно на фронте) |
| Проекции / поиск | при поиске — чтение поисковой проекции FixationRequestSearchDocument (scope-фильтр, ADR-0059); PostgreSQL-индексы фильтров: ix_fixation_requests_origin_agency_company_id, ix_fixation_requests_executor_agent_user_id, ix_fixation_requests_status, ix_fixation_requests_created_at |
| Уведомления | none |
Ошибки и failure modes
Рядовой агент не может запросить чужой ExecutorAgentUserId — попытка всегда 403 CRM_FIXATIONS_FORBIDDEN.
Несуществующие/чужие значения фильтров (DeveloperCompanyId, Status) не считаются ошибкой — они дают
пустую страницу.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Take/Offset вне диапазона, неизвестный Status, SearchTerm > 128, пустой Guid-фильтр | «Проверьте параметры запроса» | no retry |
CRM_FIXATIONS_FORBIDDEN | 403 | нет AgencyCompanyId в claims; агент запросил ExecutorAgentUserId != self | «Недостаточно прав для просмотра фиксаций» | no retry |
UNAUTHORIZED | 401 | нет/невалидный JWT или отсутствует crm.fixations.view | «Требуется авторизация» | no retry |
CRM_FIXATIONS_SEARCH_UNAVAILABLE | 503 | SearchTerm задан, но поисковая проекция FixationRequestSearchDocument (Elasticsearch) недоступна | «Поиск временно недоступен, повторите позже» | retry |
Базовый список (без SearchTerm) читается из PostgreSQL по индексам фильтров и не зависит от
Elasticsearch; деградирует только путь с поиском (см. ../../README.md → зависимости).
Совместимость и наблюдаемость
- Новые optional фильтры/поля ответа — additive (без breaking). Удаление/переименование полей или сужение enum
FixationStatus/FixationUniqueness— breaking (миграция клиента + rollout); неизвестный член enum на клиенте — по правилу unknown-enum. - Logs:
agent-fixations listed(ActorUserId,ActorCompanyId,Total, флагfts; телефон/ФИО — не пишутся); metrics:crm_fixations_list_total{scope=own|agency,fts}, latency p95 (< 1 c, SLO списков); traces — сквозной trace черезtraceparent; dashboardpin-crm.
Acceptance Criteria
- Happy path. Given агент без фильтров; When
GET; Then200— свои фиксации (ExecutorAgentUserId = actor), сортировкаCreatedAt desc;ClientPhoneNumberв ответе без маски (собственные данные агентства). - Фильтр по агенту. Given руководитель агентства с фильтром
ExecutorAgentUserId; WhenGET; Then200— фиксации указанного исполнителя в границахOriginAgencyCompanyId. - Scope запрещён. Given рядовой агент задал
ExecutorAgentUserId != ActorUserId; WhenGET; Then403 CRM_FIXATIONS_FORBIDDEN. - Поиск. Given заданный
SearchTerm; WhenGET; Then multi-match (fuzzy) по ФИО + телефону, сортировка по relevance desc. - Фильтры. Given
DeveloperCompanyId/Status; WhenGET; Then выдача сужена; несуществующие значения дают пустую страницу (не404). - Валидация. Given
Take/Offsetвне диапазона, неизвестныйStatusилиSearchTermдлиннее 128; WhenGET; Then400 VALIDATION_FAILED. - Деградация поиска. Given задан
SearchTerm, но проекцияFixationRequestSearchDocumentнедоступна; WhenGET; Then503 CRM_FIXATIONS_SEARCH_UNAVAILABLE(retry); запрос без поиска отрабатывает штатно.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/developer/crm/fixations — Список фиксаций застройщика (+ FTS), GET /api/pin-manager/crm/fixations — Сквозной список всех фиксаций платформы
- Поиск (1): FixationRequestSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Дом сущности и правило дубль-детекта: fixation-request.md; поисковая
проекция — ../../search/fixations.md (
FixationRequestSearchDocument, ADR-0059); VO ФИО — client.md; лид-основание — lead.md. - События решений (не публикуются этим методом): fixation-approved.md, fixation-rejected.md.
- Смежные методы области: create-booking-request.md; карта area agent — README.md. ADR: 0052 tenancy, 0014 repository-pipeline и scope-filters, 0059 Elasticsearch-проекции.