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 area | investor |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/list-deals.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Инвестор) |
| Auth policy / Permissions | crm.deals.view (JWT инвестора); area-гейт «investor» |
| Scope (RBAC) | платформа (инвестор вне developer/agency-tenant — кросс-tenant участник, ADR-0052) |
| Record-level | PrimaryInvestorUserId = actor — форсится сервером, чужие сделки недостижимы |
| Tenant isolation | Запрос не tenanted (Deal.TenantId = DeveloperCompanyId у сделок разный): чтение по явному record-level предикату PrimaryInvestorUserId = actor (ADR-0052, «вторая сторона») |
| Audit | read (не sensitive: без номера эскроу-счёта и данных фиксаций); ФИО/телефон/цена в логи не пишутся |
Инвестор видит только собственные сделки: PrimaryInvestorUserId = actor форсится сервером и не принимается
из query/тела. Сделки инвестора живут в разных developer-tenant, поэтому охват — record-level предикат
PrimaryInvestorUserId = actor (ADR-0052), а не TenantId. Данные о фиксациях в выдачу не входят.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/investor/crm/deals |
| Success status | 200 |
| Idempotency header | не применяется (read-only, безопасный метод) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
PrimaryInvestorUserId не передаётся клиентом: значение всегда берётся из токена, а не из query — попытка
подставить чужой id в query игнорируется.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PrimaryInvestorUserId | identity claims | Guid | Да | Required (= actor); задаётся сервером, из query/тела не принимается | Record-level (own-only) |
StatusFilter | query | InvestorDealStatusFilter? | Нет | enum строкой (см. ниже); null = все; неизвестное → VALIDATION_FAILED | — |
Paging | query | пагинация | Нет | Take 1..200 (default 20), Offset >= 0 (default 0) | защита от unbounded |
Sort | query | сортировка по 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 | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id | — | dealId для маршрута /profile/deals/[dealId]. |
Title | string | Да | DealEntity.Title | — | Заголовок сделки — объект («Объект №…, ЖК …»). |
PropertyObjectId | Guid | Да | DealEntity.PropertyObjectId | — | Объект сделки (логическая ссылка chessboard); display-данные объекта недвижимости витрина дозапрашивает отдельно. |
Status | DealStatus | Да | DealEntity.Status (enum строкой) | — | Статус процесса, US-INV-03: заявка/бронь/продажа/отказ (Completed → «продажа (завершена)»; дом — deal.md). |
Price | Money | Да | DealEntity.Price (owned Amount,Currency) | — | Полная стоимость объекта недвижимости (snapshot chessboard на момент создания/смены). |
CreatedAt | DateTimeOffset | Да | DealEntity.CreatedAt | — | Дата создания. |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | Дата последнего обновления (US-INV-04). |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор;
PrimaryInvestorUserId = actorберётся из токена (из query не принимается); permissioncrm.deals.view(иначеCRM_DEALS_FORBIDDEN);Paging/StatusFilter/Sort— из query. - Проверка входа и предусловий.
Paging.Take ∈ 1..200,Offset >= 0, значенияStatusFilter/Sort.SortBy— известные enum, иначеVALIDATION_FAILED. Существование по id не проверяется (нет входных id-фильтров — выборка по своемуPrimaryInvestorUserId); чужой инвестор недостижим (любойPrimaryInvestorUserIdиз запроса игнорируется). - Что читаем. Страницу собственных сделок инвестора: предикат
PrimaryInvestorUserId = actor(при заданномStatusFilter— плюсStatus ∈ <набор>), порядок и срез — поSort/Paging; удалённые записи скрыты. Join к фиксациям/клиентам не выполняется. Отдельно — общее число строк той же выборки для метаданных страницы. - Бизнес-правила и маппинг. Переходов нет. Разворот фильтра:
Active → {Application, Booking, Sale},Completed → {Completed},Cancelled → {Cancelled}. Порядок — поSort.SortBy(CreatedAt/UpdatedAt) иDirection(defaultCreatedAt Desc), tie-break поId. Маппинг поле-в-поле — таблица «Модель ответа» (7 полей проекции, без фиксаций/эскроу). - Результат. Постраничный список
InvestorDealListItemModel(Items+ метаданные страницы) → HTTP200; номер эскроу-счёта и данные фиксаций в проекцию не входят; пустойItemsпри отсутствии сделок — тоже200(не 404).
Диаграмма
Пустой список — не ошибка: если у инвестора нет сделок (или ни одна не проходит StatusFilter), ответ —
200 с Items = [], Total = 0, а не 404.
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (read-only) |
| External calls | none (display-данные объекта недвижимости/проекта витрина дозапрашивает в chessboard отдельно) |
| Cache / projections / search | none (прямой запрос PostgreSQL по ix_deals_primary_investor_user_id) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | неверные Take/Offset, неизвестный enum StatusFilter/Sort.SortBy | «Проверьте параметры фильтра» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_DEALS_FORBIDDEN | 403 | нет 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. Тогда:200cItems = [],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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор), GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы, POST /api/investor/crm/deals/{dealId}/acknowledge-terms — Отметить «Ознакомлен»
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей,
DealStatus,Money, индексix_deals_primary_investor_user_id): deal.md. - События жизненного цикла: deal-created.md, deal-completed.md, deal-cancelled.md.
- Соседние операции инвестора: get-deal.md, get-deal-timeline.md, list-leads.md.
- Контекст: README; tenancy — ADR-0052.