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

GET /api/v1/internal/selections/by-owner/{ownerUserId} — Свои подборки инвестора (m2m crm)

Синхронное internal-чтение списка собственных подборок инвестора-владельца по его UserId. Метод адресован доверенному сервису crm (m2m): при открытии карточки клиента/лида агент видит блок «личные подборки клиента» — подборки, которые инвестор создал сам, чтобы агент понимал круг интереса клиента. В отличие от расшаренных/просмотренных подборок (те уже приходят в ленту лида событиями selection-shared и user-activity), здесь возвращаются именно свои подборки инвестора как владельца строки.

Назначение

crm резолвит investorUserId из лида/сделки и, убедившись в активном лиде агента на этого инвестора, вызывает метод с доверенным ownerUserId = investorUserId. Сервис selections фильтрует по владельцу строки (OwnerUserId == ownerUserId), исключает soft-deleted и отдаёт компактную проекцию (без состава и без OwnerUserId, равного входу). Право агента на инвестора обеспечивает сам crm (delegated), поэтому selections доверяет ownerUserId из маршрута и record-level по актору не накладывает. Display-данные объектов недвижимости здесь не отдаются — при открытии конкретной подборки CRM читает её составом через get-selection.md и добирает карточки батчем в catalog. Дом полей — ../domain/selection.md.

Metadata

ПолеЗначение
Сервис/контекстselections
API areainternal
Feature groupselections
Статусdraft
Документdocs/06-services/selections/api/internal/list-selections-by-owner.md

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

ПроверкаЗначение
Actor typeExternalSystem — m2m-клиент crm (client-credentials)
Auth policy / Permissionspermission selections.read-by-owner-internal (m2m-клиент crm); пользовательского пути к операции нет
Scope (RBAC)не применяется; право агента на инвестора (активный лид) проверяет crm до вызова (delegated, паттерн UC-ACT-004)
Record-levelфильтр по владельцу строки OwnerUserId == ownerUserId (возвращаются только собственные подборки инвестора); record-level по актору-агенту не накладывается — crm доверенный вызыватель
Tenant isolationне применяется — подборка личная, вне tenant (ADR-0052 п.2)
Auditread-sensitive access-audit: actor (service), ownerUserId (targetUserId); PII нет

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/internal/selections/by-owner/{ownerUserId:guid}
Success status200 (в т.ч. пустой список — Items = [], не 404)
Idempotency headerне применяется (безопасный read)
Correlationсквозной trace через заголовок traceparent, из crm

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
OwnerUserIdroute ({ownerUserId})GuidДа!= Guid.Empty; = investorUserId, резолвится crm из лидадоверенный m2m; record-level фильтр выборки по этому значению
PagequeryintНет (default 1)>= 1
PageSizequeryintНет (default 20)1..100

Body отсутствует.

Модель ответа InternalSelectionsByOwnerResult

ПолеТипОбязательностьИсточникMaskingОписание
ItemsIReadOnlyList<InternalOwnerSelectionListItemModel>Да (может быть пуст)проекция выборкиСтраница собственных подборок инвестора.
TotalCountintДаCount того же предикатаВсего не удалённых подборок владельца (≤ 100, INV-SEL-007).
PageintДаecho запросаТекущая страница.
PageSizeintДаecho запросаРазмер страницы.

InternalOwnerSelectionListItemModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаSelection.IdИдентификатор подборки — для последующего чтения составом (get-selection.md).
TitlestringДаSelection.TitleНазвание подборки.
PublicCodestringДаSelection.PublicCodeПубличный код ссылки /collection/{publicCode} — открытие из карточки клиента.
PropertyObjectsCountintДаcomputed: Count(SelectionItem where SelectionId == Id)Объектов недвижимости в подборке (0..200); состав не материализуется.
UpdatedAtDateTimeOffsetДаSelection.UpdatedAtПоследняя правка — для сортировки «свежие сверху».

OwnerUserId в элемент не включается (равен входному ownerUserId); Comment/состав элементов не отдаются (приватность и объём) — состав читается отдельной операцией по Id. Заметки инвестора кросс-владельца никогда не просачиваются: этот метод возвращает только подборки, где владелец — сам инвестор.

Алгоритм

  1. Контекст и доступ. Доверенный m2m crm (client-credentials, permission selections.read-by-owner-internal); вызыватель без permission → 403 FORBIDDEN. Активный лид агента на инвестора проверяет crm до вызова (delegated, UC-ACT-004); selections доверяет ownerUserId из маршрута.
  2. Проверка входа и предусловий. OwnerUserId != Guid.Empty, Page >= 1, PageSize ∈ 1..100 — иначе VALIDATION_ERROR.
  3. Что читаем. Страница подборок владельца, не удалённых (DeletedAt == null, INV-SEL-009), в порядке UpdatedAt desc, с проекцией InternalOwnerSelectionListItemModel (PropertyObjectsCount — счётчик элементов подзапросом, состав не материализуется); плюс общее число подборок владельца тем же условием. Несуществующий/безактивный ownerUserId (чужой контекст identity синхронно не проверяется) → пустая страница, не 404.
  4. Результат. Модель InternalSelectionsByOwnerResult (страница + TotalCount + echo Page/PageSize) → 200. Freshness: read-your-writes из основной БД (реплик нет, ADR-0056).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400OwnerUserId == Guid.Empty или Page/PageSize вне диапазона«Некорректные параметры запроса»no retry
FORBIDDEN403Нет permission selections.read-by-owner-internal (в т.ч. пользовательский токен)no retry
— (не ошибка)200Неизвестный / безактивный ownerUserId — пустой результат

Кросс-сервисный контракт (selectionscrm)

  • selections ожидает от crm: валидный m2m client-credentials токен с permission selections.read-by-owner-internal; доверенный ownerUserId (= investorUserId), резолвленный из лида/сделки. Record-level «активный лид агента на инвестора» проверяет сам crm (delegated, UC-ACT-004) — selections не перепроверяет и по актору-агенту не фильтрует.
  • selections предоставляет crm: страницу собственных подборок инвестора (владелец строки = ownerUserId), без soft-deleted (INV-SEL-009), в порядке UpdatedAt desc, компактной проекцией (Id, Title, PublicCode, PropertyObjectsCount, UpdatedAt) — без состава и без OwnerUserId. Расшаренные агенту и просмотренные инвестором подборки сюда не входят (они уже в ленте лида через selection-shared/user-activity).
  • Поведение при пустоте/устаревании: неизвестный ownerUserId200 с Items = [] (не 404); crm не кэширует список — при каждом открытии карточки читает актуальную страницу, поэтому создание/ переименование/удаление подборок инвестором видны сразу.

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

  • Предусловия: вызыватель — доверенный m2m crm с permission selections.read-by-owner-internal; crm подтвердил активный лид агента на инвестора; OwnerUserId непустой.
  • Постусловия: состояние не меняется (безопасное чтение); возвращается страница только не удалённых подборок, где владелец — сам инвестор (INV-SEL-002/009), в порядке UpdatedAt desc; TotalCount считает подборки по тому же предикату (≤ 100, INV-SEL-007); Comment/состав кросс-владельца не отдаются; данные актуальны (read-your-writes, реплик нет — ADR-0056); зафиксирован read-sensitive access-audit.

Acceptance Criteria

  • Given инвестор с 3 своими не удалёнными подборками, When crm вызывает GET /internal/selections/by-owner/{ownerUserId}, Then 200, Items — 3 подборки по убыванию UpdatedAt, у каждого элемента есть Id, Title, PublicCode, PropertyObjectsCount, UpdatedAt; TotalCount = 3.
  • Given у инвестора есть soft-deleted подборка, When GET, Then она не входит ни в Items, ни в TotalCount (INV-SEL-009).
  • Given инвестор без собственных подборок либо неизвестный ownerUserId, When GET, Then 200, Items = [], TotalCount = 0 (не 404).
  • Given подборка, которую инвестору лишь расшарили (владелец — агент), When GET по ownerUserId инвестора, Then она в результат не попадает — возвращаются только подборки, где владелец = инвестор.
  • Given ownerUserId с 25 подборками, When GET?page=2&pageSize=20, Then 200, Items — оставшиеся 5, TotalCount = 25.
  • Given вызыватель без permission selections.read-by-owner-internal (другой m2m или пользовательский токен), When GET, Then 403 FORBIDDEN.
  • Given ownerUserId == Guid.Empty или pageSize = 0/page = 0, When GET, Then 400 VALIDATION_ERROR.
  • Given агент без активного лида на инвестора, When открывает карточку, Then crm не вызывает selections и отдаёт 403 в свой фронт (delegated-авторизация на стороне crm, UC-ACT-004).

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

  • Новые optional поля ответа — additive; удаление/переименование/смена типа — breaking (migration + rollout, согласование с crm). Проекция не содержит OwnerUserId/состава/Comment — приватность инвестора.
  • Logs: OwnerUserId (targetUserId), Page, счётчик результата (без Title); metrics: selections_internal_by_owner_total{result}; traces: сквозной span из crm. Dashboard — selections-overview; alert — рост 5xx internal-области (деградация карточки клиента в CRM, ../README.md).

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

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

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