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

GET /api/developer/crm/deals — Сделки застройщика (список/поиск)

Список всех сделок по проектам застройщика для канбан-доски и реестра сделок ЛК застройщика (US-DEV-01). Read-only проекция агрегата DealEntity с фильтрами по проекту/зданию/статусу/этапу и полнотекстовым поиском (FTS по Title + ФИО клиента), сортировкой по дате создания. Разграничение — по TenantId = DeveloperCompanyId (ADR-0052): застройщик видит только свои сделки. Мутаций и событий не производит.

Назначение

Отдать менеджеру застройщика постранично сделки его компании (заголовок, ФИО клиента-покупателя, объект, проект, статус/этап канбана, цена, тип договора и способ оплаты) для доски /crm и карточек-строк реестра.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика)
Auth policy / Permissionscrm.deals.view (JWT identity)
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelне применяется: застройщик видит все сделки своего TenantId (лиды/клиентскую базу агентства — нет)
Tenant isolationTenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически
Auditread (не sensitive): ФИО клиента-покупателя в логи не пишется
Доступ

Record-level ограничения внутри тенанта не применяются: застройщик видит все сделки своего TenantId (= DeveloperCompanyId). Это не доступ к клиентской базе агентства — Client.ClientName приходит join'ом только по контрагенту конкретной сделки.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (DeveloperCompanyId)Tenant isolation
PagingqueryPagedQueryНетTake 1..200 (default 20), Offset >= 0 (default 0)
SortquerySortQuery<DealSortField>?Нетполе — enum CreatedAt(default)/UpdatedAt; direction default Desc
Searchquerystring?Нетtrim, 2..128; полнотекстовый поиск по проекции DealSearchDocument (multi-match, fuzzy)PII: не в логи
ProjectIdqueryGuid?Нет!= Guid.Empty; несуществующий/чужой → пустой список (scope+filter), без 404
BuildingIdqueryGuid?Нет!= Guid.Empty; здание/литер проекта
StatusqueryDealStatus?Нетenum строкой Application/Booking/Sale/Completed/Cancelled; unknown → ValidationError
StepIdqueryGuid?Нет!= Guid.Empty; этап канбана застройщика (фильтр колонки)
StepGroupqueryPipelineStepGroup?Нетenum строкой (ApplicationCancelled); быстрый фильтр по системной группе

Сортировка типизирована SortQuery<DealSortField> (enum, не строка); default CreatedAt Desc, tiebreak Id desc. Основной индекс — (tenant_id, status, created_at desc) (см. deal.md, «Индексы»). Status/StepGroup/ProjectId/BuildingId/StepId — комбинируемые предикаты (AND).

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

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

DealListItemModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealEntity.IdИдентификатор сделки (навигация в карточку).
TitlestringДаDealEntity.TitleЗаголовок карточки («Объект №…, ЖК …»).
ClientClientRefModel ({ ClientId: Guid, ClientName: string })Даjoin crm.clients по DealEntity.ClientIdPII (ФИО): не в логиКонтрагент сделки.
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdОбъект сделки (логическая ссылка chessboard).
ProjectIdGuidДаDealEntity.ProjectIdПроект объекта (фильтр/группировка).
StatusDealStatusДаDealEntity.Status (enum строкой)Application/Booking/Sale/Completed/Cancelled.
StepIdGuidДаDealEntity.StepIdТекущий настраиваемый этап канбана застройщика.
StepGroupPipelineStepGroupДаDealEntity.StepGroup (enum строкой)Системная группа этапа (pipeline-step.md).
PriceMoneyViewДаDealEntity.PriceПолная стоимость объекта недвижимости (snapshot chessboard).
ContractTypeDealContractType?НетDealEntity.ContractType (enum строкой)Ddu/Dkp; null до перехода в группу Contract.
PaymentMethodPaymentMethod?НетDealEntity.PaymentMethod (enum строкой)Способ оплаты (purchase-program.md).
CreatedAtDateTimeOffsetДаDealEntity.CreatedAtДата создания (ключ сортировки по умолчанию).

Price наружу — MoneyView (Amount, CurrencyCode, Formatted); все enum — строкой; дом значений — deal.md.

Алгоритм

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

  1. Контекст и доступ. Выполняет менеджер застройщика (User) в своём tenant (TenantId = DeveloperCompanyId); требуется permission crm.deals.view, иначе CRM_DEALS_FORBIDDEN. Параметры — пагинация, сортировка, фильтры и поисковая строка.
  2. Проверка входа. Paging.Take ∈ 1..200, Offset >= 0; Sort.Field — известное значение enum; Status/StepGroup — известные значения enum (unknown → VALIDATION_FAILED, не «тихий» фильтр); Search2..128 после trim; ProjectId/BuildingId/StepId != Guid.Empty при заданных. Существование по id не проверяется — это фильтры списка (несуществующий/чужой ProjectId/StepId → пустая страница, без 404). Scope — только tenant; record-level не применяется.
  3. Что читаем. Сделки застройщика с фильтрами Status/StepId/StepGroup/ProjectId/BuildingId (AND). При заданном Search — предварительный отбор DealId через поисковую проекцию DealSearchDocument (multi-match по Title + ФИО клиента, fuzzy; scope = TenantId, ADR-0059), затем предикат Id ∈ ids. Разграничение по TenantId = DeveloperCompanyId и отсечение soft-deleted применяются автоматически. Пагинация и сортировка CreatedAt desc → Id desc; опорный индекс — (tenant_id, status, created_at desc) (deal.md, «Индексы»).
  4. Бизнес-правила и маппинг. Переходов состояний нет. Маппинг поле-в-поле — таблица «Модель ответа»; StepGroup берётся из денормализованной колонки DealEntity.StepGroup (без join на pipeline_steps); ClientName — из join crm.clients (застройщик видит только контрагента своих сделок, не клиентскую базу агентства). Enum — строкой, Price → MoneyView.
  5. Результат. Страница DealListItemModel (Items + Meta { Total, Take, Offset, HasMore }) → HTTP 200. Пустой результат — 200 с Items = [] (не 404).

Диаграмма

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

ТипДетали
Integration eventsnone (read-only)
External callsnone (данные — своя схема crm; PropertyObjectId/ProjectId/цена — snapshot из chessboard на момент создания сделки)
Cache / projections / searchпрямой запрос PostgreSQL; при Search — чтение поисковой проекции DealSearchDocument (multi-match, scope по tenant, ADR-0059)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Take вне 1..200, неизвестный Status/StepGroup/Sort.Field, Search короче 2, Guid.Empty в фильтре«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DEALS_FORBIDDEN403нет permission crm.deals.view«Недостаточно прав»no retry
Важно

Неизвестное значение Status/StepGroup/Sort.Field — это 400 VALIDATION_FAILED, а не молчаливый пропуск фильтра. Несуществующий или чужой ProjectId/BuildingId/StepId, напротив, не ошибка: это фильтр списка, результат — пустая страница 200.

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

  • Новые optional поля ответа/фильтры — additive (без breaking); удаление/переименование/смена типа поля или значения enum — breaking (migration + rollout). Unknown enum во входе — ValidationError (не игнор).
  • Logs: developer deals listed (TenantId, набор фильтров, счётчик Total; ФИО клиента и Search — не в логи); metrics: crm_developer_deals_list_total{result}, latency p95 (< 1 c, SLO списков); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

Acceptance criteria

  • Given сделки застройщика, When менеджер запрашивает список без фильтров, Then 200 со страницей DealListItemModel в порядке CreatedAt desc → Id desc + Meta { Total, Take, Offset, HasMore }.
  • Given заданы Status/StepId/StepGroup/ProjectId/BuildingId, When GET, Then предикаты комбинируются по AND; пустое пересечение — 200 с Items = [] (не 404).
  • Given Search длиной 2..128, When GET, Then предварительный отбор идёт через полнотекстовую проекцию DealSearchDocument (multi-match по Title + ФИО клиента, fuzzy, scope = TenantId), затем фильтры; сортировка — по релевантности.
  • Given неизвестное значение Status/StepGroup/Sort.Field, When GET, Then 400 VALIDATION_FAILED (unknown enum — ошибка, а не «тихий» фильтр).
  • Given ProjectId/StepId несуществующий или чужой, When GET, Then 200 с пустой страницей (это фильтр, не сущность).
  • Given Take вне 1..200 или Search короче 2, When GET, Then 400 VALIDATION_FAILED.
  • Given актор без crm.deals.view, When GET, Then 403 CRM_DEALS_FORBIDDEN; ФИО клиента и строка поиска в логи не пишутся.

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

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

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