GET /api/agent/crm/deals — Сделки агента (список/поиск)
Список сделок клиентов агента для рабочего пространства агентства (/crm/deals, US-AGT-01): свои сделки или —
для руководителя агентства — сделки всех агентов агентства (переключатель «свои/все»), с фильтрами по клиенту/
объекту/статусу, диапазонами дат создания и обновления и полнотекстовым поиском по Title + ФИО клиента.
Read-only проекция агрегата DealEntity (deal.md); карточка сделки, канбан и «Мои сделки»
инвестора — соседние операции.
Назначение
Отдать агенту/руководителю агентства страницу его сделок с фильтрами и полнотекстовым поиском (сценарий US-AGT-01: агент ведёт клиентов и их сделки у разных застройщиков).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/list-deals.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент; Руководитель агентства — расширенный охват «все») |
| Auth policy / Permissions | permission crm.deals.view |
| Scope (RBAC) | организация (агентство OriginAgencyCompanyId) |
| Record-level | OriginAgencyCompanyId = actor.AgencyCompanyId; агент — актор как активный участник-исполнитель (ExecutorAgentUserId = actor); руководитель — все агенты агентства либо конкретный ExecutorAgentUserId. Кросс-застройщик по замыслу: сделки живут в разных developer-tenant, агентская сторона выбирается record-level колонками (ADR-0052) |
| Tenant isolation | не developer-tenanted: Deal.TenantId = DeveloperCompanyId, агент читает сделки многих застройщиков; изоляция — record-level по OriginAgencyCompanyId из claims |
| Audit | read (не sensitive: без номера эскроу-счёта); PII (ФИО/телефон) в логи не пишется |
Рядовой агент видит только свои сделки (ExecutorAgentUserId = actor); руководитель агентства — переключатель
«свои/все»: без фильтра — сделки всех агентов агентства, с фильтром — сделки конкретного агента-исполнителя.
Сделки кросс-застройщик: Deal.TenantId — это DeveloperCompanyId разных застройщиков, поэтому изоляция
строится по OriginAgencyCompanyId из claims (ADR-0052), а не по TenantId.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/agent/crm/deals |
| Success status | 200 |
| Idempotency header | не применяется (read-only, безопасный метод) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
AgencyCompanyId | identity claims | Guid | Да | Required; record-level предикат OriginAgencyCompanyId | scope isolation |
ExecutorAgentUserId | query | Guid? | Нет | переключатель «свои/все» (по текущему исполнителю): агент — игнорируется, форсится = actor; руководитель — null = все агенты агентства, заданный = конкретный агент-исполнитель | record-level |
ClientId | query | Guid? | Нет | фильтр по клиенту; несуществующий id → пустая страница (не 404) | — |
PropertyObjectId | query | Guid? | Нет | фильтр по объекту (логическая ссылка chessboard) | — |
Status | query | DealStatus? | Нет | enum строкой (deal.md); неизвестное значение → ValidationError | — |
StepGroup | query | PipelineStepGroup? | Нет | enum строкой (pipeline-step.md); быстрый фильтр группы канбана | — |
Created | query | NullableRange<DateTimeOffset> | Нет | диапазон CreatedAt {From, To}; From ≤ To | — |
Updated | query | NullableRange<DateTimeOffset> | Нет | диапазон UpdatedAt {From, To}; From ≤ To | — |
Search | query | string? | Нет | поиск по DealSearchDocument (Title + ФИО), max 256, trim; multi-match (fuzzy) | вход untrusted → поисковый запрос, не raw-DSL |
Page | query | int | Нет | default 1, >= 1 | — |
PageSize | query | int | Нет | default 20, 1..100 | защита от unbounded |
SortBy | query | DealListSortBy? | Нет | enum CreatedAt / UpdatedAt / Price; default CreatedAt | — |
SortDescending | query | bool | Нет | default true | — |
Enum DealListSortBy
| Значение | Сортировка |
|---|---|
CreatedAt | по дате создания (default). |
UpdatedAt | по дате последнего обновления (US-INV-04). |
Price | по полной стоимости объекта (Money.Amount). |
Модель ответа — постраничный список DealListItemModel
Постраничный контейнер: Items : IReadOnlyList<DealListItemModel>, TotalCount : long, Page : int,
PageSize : int. Дом полей сделки — deal.md; ниже — только состав строки списка.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id | — | dealId для маршрута /crm/deals/[dealId]. |
Title | string | Да | DealEntity.Title | — | Заголовок сделки («Объект №…, ЖК …»). |
ClientName | FullName | Да | ClientEntity.Name (по ClientId) | владельцу агентства — полностью | ФИО клиента (VO FullName, client.md). |
PropertyObjectId | Guid | Да | DealEntity.PropertyObjectId | — | Объект сделки (chessboard); display-данные — отдельным запросом витрины. |
Status | DealStatus | Да | DealEntity.Status | — | Строкой (заявка/бронь/продажа/завершена/отказ). |
StepGroup | PipelineStepGroup | Да | DealEntity.StepGroup | — | Строкой; системная группа текущего этапа канбана. |
Price | Money | Да | DealEntity.Price (owned Amount,Currency) | — | Полная стоимость объекта (snapshot). |
CreatedAt | DateTimeOffset | Да | DealEntity.CreatedAt | — | Дата создания. |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | Дата последнего обновления (US-INV-04). |
BookingPaymentStatus | BookingPaymentStatus? | Нет | BookingEntity.PaymentStatus активной брони (DealEntity.BookingId) | — | Статус оплаты брони строкой (booking.md); null — активной брони нет. |
Алгоритм
- Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission
crm.deals.view); якорь —AgencyCompanyIdиз claims. - Проверка входа и предусловий.
- формат:
Page >= 1,PageSize 1..100,Created.From <= Created.To,Updated.From <= Updated.To,Search ≤ 256, значенияStatus/StepGroup/SortBy— известные enum — иначе ошибка валидации; - несуществующие
ClientId/PropertyObjectId/ExecutorAgentUserIdдают пустую страницу (это фильтры, не 404); - охват агента: если actor не руководитель и
ExecutorAgentUserIdзадан и!= actor→CRM_DEALS_AGENT_SCOPE_FORBIDDEN; иначе агент форсится наExecutorAgentUserId = actor, руководитель сExecutorAgentUserId = nullвидит всё агентство.
- формат:
- Что читаем. Проекцию строк списка со связями
Deal → Client(ФИО) иDeal → Booking(PaymentStatus); record-level предикатOriginAgencyCompanyIdиз claims (+ охват агента-исполнителя), опциональные фильтры (клиент, объект, статус, группа, диапазоны дат,DeletedAt == null). ПриSearch— предварительный отбор идентификаторов сделок через поисковую проекциюDealSearchDocument(scope-фильтр, ADR-0059), затем ограничение по этим id. ОтдельныйTotalCountпо тому же предикату. - Правила сортировки и маппинг.
orderByпоSortBy(CreatedAt/UpdatedAt/Price.Amount) сSortDescending, tie-break поId. Маппинг поле-в-поле — таблица «Модель ответа». - Результат. Постраничный список (
Items,TotalCount,Page,PageSize) → HTTP200; номер эскроу-счёта и данные фиксаций в список не входят; пустойItemsпри отсутствии совпадений — тоже200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (read-only) |
| Внешние вызовы | none (display-данные объекта/проекта витрина дозапрашивает в chessboard отдельно) |
| Проекции / поиск | при Search — чтение поисковой проекции DealSearchDocument (scope-фильтр, ADR-0059); проекций не пишет |
| Уведомления | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | неверные Page/PageSize, From > To, неизвестный enum Status/StepGroup/SortBy | «Проверьте параметры фильтра» | no retry |
FORBIDDEN | 403 | нет permission crm.deals.view | «Недостаточно прав» | no retry |
CRM_DEALS_AGENT_SCOPE_FORBIDDEN | 403 | агент (не руководитель) запросил ExecutorAgentUserId другого агента | «Доступны только ваши сделки» | no retry |
CRM_DEALS_SEARCH_UNAVAILABLE | 503 | Search задан, но поисковая проекция DealSearchDocument (Elasticsearch) недоступна | «Поиск временно недоступен, повторите позже» | retry |
Базовый список (без Search) читается из PostgreSQL и не зависит от Elasticsearch: при недоступной
проекции запросы без поиска работают штатно, деградирует только путь с Search (см.
../../README.md → зависимости).
Совместимость и наблюдаемость
- Новые optional query-параметры и поля
DealListItemModel— additive, без breaking change; удаление/ переименование/смена типа поля или enum — breaking (migration + rollout).DealStatus/PipelineStepGroup/BookingPaymentStatus— enum строкой; неизвестное значение наружу не отдаётся. - Logs:
agent-deals-listed(OriginAgencyCompanyId,ExecutorAgentUserId?, число фильтров,TotalCount; ФИО/телефон не пишутся); metrics:crm_deals_list_total{result}, latency p95 (SLO списков < 1 c); traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given агент без фильтров; When
GET; Then200— свои сделки, сортировкаCreatedAt desc; эскроу-счёт и данные фиксаций в список не входят. - Всё агентство. Given руководитель и
ExecutorAgentUserId = null; WhenGET; Then200— сделки всех агентов агентства (кросс-застройщик по record-levelOriginAgencyCompanyId). - Scope запрещён. Given агент (не руководитель) задал
ExecutorAgentUserIdдругого агента; WhenGET; Then403 CRM_DEALS_AGENT_SCOPE_FORBIDDEN. - Поиск. Given заданный
Search; WhenGET; Then предварительный отбор id черезDealSearchDocument, выдача только совпавших сделок. - Фильтр без совпадений. Given несуществующий
ClientId/PropertyObjectId; WhenGET; Then200с пустой страницей (не404). - Валидация. Given
From > To, неизвестныйStatus/StepGroup/SortByилиPageSizeвне1..100; WhenGET; Then400 VALIDATION_FAILED. - Оплата брони. Given сделка с активной бронью; When
GET; Then строка содержитBookingPaymentStatus; без брони —null. - Деградация поиска. Given задан
Search, но проекцияDealSearchDocumentнедоступна; WhenGET; Then503 CRM_DEALS_SEARCH_UNAVAILABLE(retry); запрос безSearchотрабатывает штатно.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы
- Поиск (1): DealSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей,
DealStatus,Money, индекс(origin_agency_company_id, executor_agent_user_id, status)): deal.md; поисковая проекция — ../../search/deals.md (DealSearchDocument, ADR-0059); группы этапов — pipeline-step.md;ClientEntity.Name— client.md;BookingPaymentStatus— booking.md. - Соседние операции агента: create-client.md, extend-lead.md, публичная заявка на бронь — create-booking-request.md.
- README контекста: ../../README.md; tenancy — ADR-0052.