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.
| Поле | Значение |
|---|
| Сервис/контекст | selections |
| API area | internal |
| Feature group | selections |
| Статус | draft |
| Документ | docs/06-services/selections/api/internal/list-selections-by-owner.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — m2m-клиент crm (client-credentials) |
| Auth policy / Permissions | permission 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) |
| Audit | read-sensitive access-audit: actor (service), ownerUserId (targetUserId); PII нет |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/internal/selections/by-owner/{ownerUserId:guid} |
| Success status | 200 (в т.ч. пустой список — Items = [], не 404) |
| Idempotency header | не применяется (безопасный read) |
| Correlation | сквозной trace через заголовок traceparent, из crm |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
OwnerUserId | route ({ownerUserId}) | Guid | Да | != Guid.Empty; = investorUserId, резолвится crm из лида | доверенный m2m; record-level фильтр выборки по этому значению |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 20) | 1..100 | — |
Body отсутствует.
Модель ответа InternalSelectionsByOwnerResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Items | IReadOnlyList<InternalOwnerSelectionListItemModel> | Да (может быть пуст) | проекция выборки | — | Страница собственных подборок инвестора. |
TotalCount | int | Да | Count того же предиката | — | Всего не удалённых подборок владельца (≤ 100, INV-SEL-007). |
Page | int | Да | echo запроса | — | Текущая страница. |
PageSize | int | Да | echo запроса | — | Размер страницы. |
InternalOwnerSelectionListItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | Selection.Id | — | Идентификатор подборки — для последующего чтения составом (get-selection.md). |
Title | string | Да | Selection.Title | — | Название подборки. |
PublicCode | string | Да | Selection.PublicCode | — | Публичный код ссылки /collection/{publicCode} — открытие из карточки клиента. |
PropertyObjectsCount | int | Да | computed: Count(SelectionItem where SelectionId == Id) | — | Объектов недвижимости в подборке (0..200); состав не материализуется. |
UpdatedAt | DateTimeOffset | Да | Selection.UpdatedAt | — | Последняя правка — для сортировки «свежие сверху». |
OwnerUserId в элемент не включается (равен входному ownerUserId); Comment/состав элементов не отдаются
(приватность и объём) — состав читается отдельной операцией по Id. Заметки инвестора кросс-владельца никогда
не просачиваются: этот метод возвращает только подборки, где владелец — сам инвестор.
Алгоритм
- Контекст и доступ. Доверенный m2m
crm (client-credentials, permission
selections.read-by-owner-internal); вызыватель без permission → 403 FORBIDDEN. Активный лид агента на
инвестора проверяет crm до вызова (delegated, UC-ACT-004); selections доверяет ownerUserId из
маршрута.
- Проверка входа и предусловий.
OwnerUserId != Guid.Empty, Page >= 1, PageSize ∈ 1..100 — иначе
VALIDATION_ERROR.
- Что читаем. Страница подборок владельца, не удалённых (
DeletedAt == null, INV-SEL-009), в порядке
UpdatedAt desc, с проекцией InternalOwnerSelectionListItemModel (PropertyObjectsCount — счётчик
элементов подзапросом, состав не материализуется); плюс общее число подборок владельца тем же условием.
Несуществующий/безактивный ownerUserId (чужой контекст identity синхронно не проверяется) → пустая
страница, не 404.
- Результат. Модель
InternalSelectionsByOwnerResult (страница + TotalCount + echo Page/PageSize)
→ 200. Freshness: read-your-writes из основной БД (реплик нет, ADR-0056).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_ERROR | 400 | OwnerUserId == Guid.Empty или Page/PageSize вне диапазона | «Некорректные параметры запроса» | no retry |
FORBIDDEN | 403 | Нет permission selections.read-by-owner-internal (в т.ч. пользовательский токен) | — | no retry |
| — (не ошибка) | 200 | Неизвестный / безактивный ownerUserId — пустой результат | — | — |
Кросс-сервисный контракт (selections ↔ crm)
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).
- Поведение при пустоте/устаревании: неизвестный
ownerUserId → 200 с 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).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы