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

GET /api/investor/crm/deals — Мои сделки (инвестор)

Список всех своих сделок инвестора одним экраном ЛК (/profile/deals, US-INV-01): объект, статус, дата создания и сумма — с фильтром по статусу «активные / завершённые / отменённые». Read-only проекция агрегата DealEntity; карточка сделки, таймлайн и документы — соседние операции области investor. Сделки инвестора живут в разных developer-tenant (инвестор — кросс-tenant участник), поэтому охват — record-level по PrimaryInvestorUserId = actor, а не по TenantId. Данные о фиксациях в выдачу не входят (инвестор их не видит).

Назначение

Отдать авторизованному инвестору постранично его собственные сделки (заголовок объекта, статус процесса, цена, даты создания и обновления) для экрана /profile/deals (US-INV-01, US-INV-03/04). Владелец — контекст crm, feature group deals, сценарий US-INV-01.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Инвестор)
Auth policy / Permissionscrm.deals.view (JWT инвестора); area-гейт «investor»
Scope (RBAC)платформа (инвестор вне developer/agency-tenant — кросс-tenant участник, ADR-0052)
Record-levelPrimaryInvestorUserId = actor — форсится сервером, чужие сделки недостижимы
Tenant isolationЗапрос не tenanted (Deal.TenantId = DeveloperCompanyId у сделок разный): чтение по явному record-level предикату PrimaryInvestorUserId = actor (ADR-0052, «вторая сторона»)
Auditread (не sensitive: без номера эскроу-счёта и данных фиксаций); ФИО/телефон/цена в логи не пишутся
Доступ

Инвестор видит только собственные сделки: PrimaryInvestorUserId = actor форсится сервером и не принимается из query/тела. Сделки инвестора живут в разных developer-tenant, поэтому охват — record-level предикат PrimaryInvestorUserId = actor (ADR-0052), а не TenantId. Данные о фиксациях в выдачу не входят.

HTTP-контракт

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

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

Важно

PrimaryInvestorUserId не передаётся клиентом: значение всегда берётся из токена, а не из query — попытка подставить чужой id в query игнорируется.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
PrimaryInvestorUserIdidentity claimsGuidДаRequired (= actor); задаётся сервером, из query/тела не принимаетсяRecord-level (own-only)
StatusFilterqueryInvestorDealStatusFilter?Нетenum строкой (см. ниже); null = все; неизвестное → VALIDATION_FAILED
PagingqueryпагинацияНетTake 1..200 (default 20), Offset >= 0 (default 0)защита от unbounded
Sortqueryсортировка по InvestorDealSortFieldНетSortBy — известное значение enum; Direction Asc/Desc; default — CreatedAt Desc

Enum InvestorDealStatusFilter

Пользовательский фильтр экрана /profile/deals (3 группы) поверх DealStatus (deal.md):

ЗначениеОтображениеРазворачивается в DealStatus
ActiveАктивныеApplication, Booking, Sale (нетерминальные)
CompletedЗавершённыеCompleted
CancelledОтменённыеCancelled

Enum InvestorDealSortField

ЗначениеСортировка
CreatedAtпо дате создания (default), убывание.
UpdatedAtпо дате последнего обновления (US-INV-04), убывание.

Модель ответа: постраничный список InvestorDealListItemModel

Постраничный контейнер: элементы InvestorDealListItemModel + метаданные страницы (Total, Take, Offset, HasMore). Дом полей сделки — deal.md; ниже — только состав строки списка (проекция, не переописание сущности). Полей фиксаций/эскроу/клиентской базы агентства в проекции нет.

InvestorDealListItemModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealEntity.IddealId для маршрута /profile/deals/[dealId].
TitlestringДаDealEntity.TitleЗаголовок сделки — объект («Объект №…, ЖК …»).
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdОбъект сделки (логическая ссылка chessboard); display-данные объекта недвижимости витрина дозапрашивает отдельно.
StatusDealStatusДаDealEntity.Status (enum строкой)Статус процесса, US-INV-03: заявка/бронь/продажа/отказ (Completed → «продажа (завершена)»; дом — deal.md).
PriceMoneyДаDealEntity.Price (owned Amount,Currency)Полная стоимость объекта недвижимости (snapshot chessboard на момент создания/смены).
CreatedAtDateTimeOffsetДаDealEntity.CreatedAtДата создания.
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtДата последнего обновления (US-INV-04).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор; PrimaryInvestorUserId = actor берётся из токена (из query не принимается); permission crm.deals.view (иначе CRM_DEALS_FORBIDDEN); Paging/StatusFilter/Sort — из query.
  2. Проверка входа и предусловий. Paging.Take ∈ 1..200, Offset >= 0, значения StatusFilter/ Sort.SortBy — известные enum, иначе VALIDATION_FAILED. Существование по id не проверяется (нет входных id-фильтров — выборка по своему PrimaryInvestorUserId); чужой инвестор недостижим (любой PrimaryInvestorUserId из запроса игнорируется).
  3. Что читаем. Страницу собственных сделок инвестора: предикат PrimaryInvestorUserId = actor (при заданном StatusFilter — плюс Status ∈ <набор>), порядок и срез — по Sort/Paging; удалённые записи скрыты. Join к фиксациям/клиентам не выполняется. Отдельно — общее число строк той же выборки для метаданных страницы.
  4. Бизнес-правила и маппинг. Переходов нет. Разворот фильтра: Active → {Application, Booking, Sale}, Completed → {Completed}, Cancelled → {Cancelled}. Порядок — по Sort.SortBy (CreatedAt/UpdatedAt) и Direction (default CreatedAt Desc), tie-break по Id. Маппинг поле-в-поле — таблица «Модель ответа» (7 полей проекции, без фиксаций/эскроу).
  5. Результат. Постраничный список InvestorDealListItemModel (Items + метаданные страницы) → HTTP 200; номер эскроу-счёта и данные фиксаций в проекцию не входят; пустой Items при отсутствии сделок — тоже 200 (не 404).

Диаграмма

Важно

Пустой список — не ошибка: если у инвестора нет сделок (или ни одна не проходит StatusFilter), ответ — 200 с Items = [], Total = 0, а не 404.

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

ТипДетали
Integration eventsnone (read-only)
External callsnone (display-данные объекта недвижимости/проекта витрина дозапрашивает в chessboard отдельно)
Cache / projections / searchnone (прямой запрос PostgreSQL по ix_deals_primary_investor_user_id)
Notificationsnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400неверные Take/Offset, неизвестный enum StatusFilter/Sort.SortBy«Проверьте параметры фильтра»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DEALS_FORBIDDEN403нет permission crm.deals.view / actor не инвестор платформы«Недостаточно прав»no retry

Предусловия и постусловия

Предусловия (истинны до сценария):

  • инвестор авторизован (JWT платформы) и имеет permission crm.deals.view; охват — только собственные сделки (PrimaryInvestorUserId = actor), сделки могут жить в разных developer-tenant;
  • снимки объекта (Title, Price) уже зафиксированы на сделке при создании/смене объекта — отдельная свежесть витрины chessboard для списка не требуется; актуальность Status/UpdatedAt — на момент запроса (source of truth — crm).

Постусловия (после запроса):

  • состояние не изменяется (read-only): ни одна сущность/счётчик не затронуты, события не публикуются;
  • клиент получил согласованную на момент запроса страницу своих сделок; номер эскроу-счёта и данные фиксаций в неё не входят; чужие сделки недостижимы.

Acceptance Criteria

  • Happy path. Дано: у инвестора 3 сделки в статусах Application, Completed, Cancelled. Когда: GET /api/investor/crm/deals без фильтра. Тогда: 200, все 3 строки, порядок CreatedAt Desc (tie-break по Id), в каждой — 7 полей проекции без эскроу/фиксаций.
  • Фильтр статуса. Дано: те же сделки. Когда: statusFilter=Active. Тогда: попадают только сделки в Application/Booking/Sale; Completed/Cancelled исключены.
  • Сортировка по обновлению. Когда: sortBy=UpdatedAt&direction=Desc. Тогда: порядок по UpdatedAt убыв., недавно изменённые сверху.
  • Пустой результат. Дано: у инвестора нет сделок. Когда: GET. Тогда: 200 c Items = [], Total = 0 (не 404).
  • Изоляция own-only. Дано: существует сделка другого инвестора. Когда: клиент передаёт чужой PrimaryInvestorUserId в query. Тогда: параметр игнорируется, выдаются только собственные сделки.
  • Пагинация вне диапазона. Когда: take=500 или offset<0. Тогда: 400 VALIDATION_FAILED.
  • Неизвестный enum. Когда: statusFilter=Foo или sortBy=Bar. Тогда: 400 VALIDATION_FAILED (не «тихий» фильтр).
  • Нет права. Когда: актор без crm.deals.view или не инвестор платформы. Тогда: 403 CRM_DEALS_FORBIDDEN.

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

  • Новые optional query-параметры и поля InvestorDealListItemModel — additive, без breaking change; удаление/переименование/смена типа поля или значения enum — breaking (migration + rollout). DealStatus/ InvestorDealStatusFilter/InvestorDealSortField — enum строкой; неизвестное значение наружу не отдаётся.
  • Logs: investor deals listed (PrimaryInvestorUserId, StatusFilter?, Total; ФИО/телефон/цена не пишутся); metrics: crm_deals_list_total{area="investor",result}, latency p95 (SLO списков < 1 c); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

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

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

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