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

GET /api/agent/crm/deals — Сделки агента (список/поиск)

Список сделок клиентов агента для рабочего пространства агентства (/crm/deals, US-AGT-01): свои сделки или — для руководителя агентства — сделки всех агентов агентства (переключатель «свои/все»), с фильтрами по клиенту/ объекту/статусу, диапазонами дат создания и обновления и полнотекстовым поиском по Title + ФИО клиента. Read-only проекция агрегата DealEntity (deal.md); карточка сделки, канбан и «Мои сделки» инвестора — соседние операции.

Назначение

Отдать агенту/руководителю агентства страницу его сделок с фильтрами и полнотекстовым поиском (сценарий US-AGT-01: агент ведёт клиентов и их сделки у разных застройщиков).

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/agent/list-deals.md

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

ПроверкаЗначение
Actor typeUser (Агент; Руководитель агентства — расширенный охват «все»)
Auth policy / Permissionspermission crm.deals.view
Scope (RBAC)организация (агентство OriginAgencyCompanyId)
Record-levelOriginAgencyCompanyId = actor.AgencyCompanyId; агент — актор как активный участник-исполнитель (ExecutorAgentUserId = actor); руководитель — все агенты агентства либо конкретный ExecutorAgentUserId. Кросс-застройщик по замыслу: сделки живут в разных developer-tenant, агентская сторона выбирается record-level колонками (ADR-0052)
Tenant isolationне developer-tenanted: Deal.TenantId = DeveloperCompanyId, агент читает сделки многих застройщиков; изоляция — record-level по OriginAgencyCompanyId из claims
Auditread (не sensitive: без номера эскроу-счёта); PII (ФИО/телефон) в логи не пишется
Доступ

Рядовой агент видит только свои сделки (ExecutorAgentUserId = actor); руководитель агентства — переключатель «свои/все»: без фильтра — сделки всех агентов агентства, с фильтром — сделки конкретного агента-исполнителя. Сделки кросс-застройщик: Deal.TenantId — это DeveloperCompanyId разных застройщиков, поэтому изоляция строится по OriginAgencyCompanyId из claims (ADR-0052), а не по TenantId.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
AgencyCompanyIdidentity claimsGuidДаRequired; record-level предикат OriginAgencyCompanyIdscope isolation
ExecutorAgentUserIdqueryGuid?Нетпереключатель «свои/все» (по текущему исполнителю): агент — игнорируется, форсится = actor; руководитель — null = все агенты агентства, заданный = конкретный агент-исполнительrecord-level
ClientIdqueryGuid?Нетфильтр по клиенту; несуществующий id → пустая страница (не 404)
PropertyObjectIdqueryGuid?Нетфильтр по объекту (логическая ссылка chessboard)
StatusqueryDealStatus?Нетenum строкой (deal.md); неизвестное значение → ValidationError
StepGroupqueryPipelineStepGroup?Нетenum строкой (pipeline-step.md); быстрый фильтр группы канбана
CreatedqueryNullableRange<DateTimeOffset>Нетдиапазон CreatedAt {From, To}; From ≤ To
UpdatedqueryNullableRange<DateTimeOffset>Нетдиапазон UpdatedAt {From, To}; From ≤ To
Searchquerystring?Нетпоиск по DealSearchDocument (Title + ФИО), max 256, trim; multi-match (fuzzy)вход untrusted → поисковый запрос, не raw-DSL
PagequeryintНетdefault 1, >= 1
PageSizequeryintНетdefault 20, 1..100защита от unbounded
SortByqueryDealListSortBy?Нетenum CreatedAt / UpdatedAt / Price; default CreatedAt
SortDescendingqueryboolНет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Описание
IdGuidДаDealEntity.IddealId для маршрута /crm/deals/[dealId].
TitlestringДаDealEntity.TitleЗаголовок сделки («Объект №…, ЖК …»).
ClientNameFullNameДаClientEntity.Name (по ClientId)владельцу агентства — полностьюФИО клиента (VO FullName, client.md).
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdОбъект сделки (chessboard); display-данные — отдельным запросом витрины.
StatusDealStatusДаDealEntity.StatusСтрокой (заявка/бронь/продажа/завершена/отказ).
StepGroupPipelineStepGroupДаDealEntity.StepGroupСтрокой; системная группа текущего этапа канбана.
PriceMoneyДаDealEntity.Price (owned Amount,Currency)Полная стоимость объекта (snapshot).
CreatedAtDateTimeOffsetДаDealEntity.CreatedAtДата создания.
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtДата последнего обновления (US-INV-04).
BookingPaymentStatusBookingPaymentStatus?НетBookingEntity.PaymentStatus активной брони (DealEntity.BookingId)Статус оплаты брони строкой (booking.md); null — активной брони нет.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission crm.deals.view); якорь — AgencyCompanyId из claims.
  2. Проверка входа и предусловий.
    • формат: 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 задан и != actorCRM_DEALS_AGENT_SCOPE_FORBIDDEN; иначе агент форсится на ExecutorAgentUserId = actor, руководитель с ExecutorAgentUserId = null видит всё агентство.
  3. Что читаем. Проекцию строк списка со связями Deal → Client (ФИО) и Deal → Booking (PaymentStatus); record-level предикат OriginAgencyCompanyId из claims (+ охват агента-исполнителя), опциональные фильтры (клиент, объект, статус, группа, диапазоны дат, DeletedAt == null). При Search — предварительный отбор идентификаторов сделок через поисковую проекцию DealSearchDocument (scope-фильтр, ADR-0059), затем ограничение по этим id. Отдельный TotalCount по тому же предикату.
  4. Правила сортировки и маппинг. orderBy по SortBy (CreatedAt/UpdatedAt/Price.Amount) с SortDescending, tie-break по Id. Маппинг поле-в-поле — таблица «Модель ответа».
  5. Результат. Постраничный список (Items, TotalCount, Page, PageSize) → HTTP 200; номер эскроу-счёта и данные фиксаций в список не входят; пустой Items при отсутствии совпадений — тоже 200.

Диаграмма

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

ТипДетали
Интеграционные событияnone (read-only)
Внешние вызовыnone (display-данные объекта/проекта витрина дозапрашивает в chessboard отдельно)
Проекции / поискпри Search — чтение поисковой проекции DealSearchDocument (scope-фильтр, ADR-0059); проекций не пишет
Уведомленияnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400неверные Page/PageSize, From > To, неизвестный enum Status/StepGroup/SortBy«Проверьте параметры фильтра»no retry
FORBIDDEN403нет permission crm.deals.view«Недостаточно прав»no retry
CRM_DEALS_AGENT_SCOPE_FORBIDDEN403агент (не руководитель) запросил ExecutorAgentUserId другого агента«Доступны только ваши сделки»no retry
CRM_DEALS_SEARCH_UNAVAILABLE503Search задан, но поисковая проекция 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; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given агент без фильтров; When GET; Then 200 — свои сделки, сортировка CreatedAt desc; эскроу-счёт и данные фиксаций в список не входят.
  • Всё агентство. Given руководитель и ExecutorAgentUserId = null; When GET; Then 200 — сделки всех агентов агентства (кросс-застройщик по record-level OriginAgencyCompanyId).
  • Scope запрещён. Given агент (не руководитель) задал ExecutorAgentUserId другого агента; When GET; Then 403 CRM_DEALS_AGENT_SCOPE_FORBIDDEN.
  • Поиск. Given заданный Search; When GET; Then предварительный отбор id через DealSearchDocument, выдача только совпавших сделок.
  • Фильтр без совпадений. Given несуществующий ClientId/PropertyObjectId; When GET; Then 200 с пустой страницей (не 404).
  • Валидация. Given From > To, неизвестный Status/StepGroup/SortBy или PageSize вне 1..100; When GET; Then 400 VALIDATION_FAILED.
  • Оплата брони. Given сделка с активной бронью; When GET; Then строка содержит BookingPaymentStatus; без брони — null.
  • Деградация поиска. Given задан Search, но проекция DealSearchDocument недоступна; When GET; Then 503 CRM_DEALS_SEARCH_UNAVAILABLE (retry); запрос без Search отрабатывает штатно.

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

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

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