GET /internal/favorites — Чтение избранного пользователя (m2m для selections)
Internal-операция: контекст selections читает текущее избранное пользователя при создании подборки
«из текущего избранного» (см. ../../../selections/README.md). Возвращает
только PropertyObjectId и даты — личные заметки (Note) не отдаются ни при каких условиях: заметка
приватна для владельца и не покидает контекст favorites
(../../domain/favorite.md, раздел безопасности).
Назначение
Единственный внешний читатель — сервис selections (m2m). Вызов происходит в рамках операции создания
подборки, инициированной самим пользователем-владельцем избранного; selections передаёт userId
актора своей команды. Гостевое избранное (AnonymousId) через этот endpoint не читается — подборку
может создать только авторизованный пользователь (владелец подборки обязателен).
| Поле | Значение |
|---|
| Сервис/контекст | favorites |
| API area | internal |
| Feature group | favorites-internal |
| Статус | draft |
| Документ | docs/06-services/favorites/api/internal/list-favorites.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | System (m2m: сервис selections) |
| Auth policy / Permissions | m2m client credentials (client pin-selections); allowlist клиентов internal-области на gateway; вызов из публичной сети невозможен |
| Scope (RBAC) | не применяется — доверенный внутренний вызов; ответственность за то, что userId = актор исходной операции, несёт вызывающий контекст (selections берёт его из JWT своей команды) |
| Record-level | выборка строго UserId = userId из query; гостевые записи (AnonymousId) недоступны |
| Tenant isolation | не применяется — сущности вне tenant (ADR-0052 п.2) |
| Audit | none (чтение без sensitive-полей — заметки исключены из ответа) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /internal/favorites?userId={userId} |
| Success status | 200 |
| Idempotency header | не применяется (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent из операции selections |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | query userId | Guid | Да | != Guid.Empty | доверенный вход из m2m-клиента; сопоставление с актором операции — обязанность selections |
Limit | query limit | int | Нет (default 500) | 1..1000 | ограничение размера подборки на стороне читателя |
Модель ответа InternalFavoritesListResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Items | IReadOnlyList<InternalFavoriteItemModel> | Да (может быть пуст) | projection Favorite | — | Избранное пользователя, CreatedAt desc. |
TotalCount | int | Да | count по UserId | — | Всего записей (если больше Limit — фронт selections предупредит об усечении). |
InternalFavoriteItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
FavoriteId | Guid | Да | Favorite.Id | — | Идентификатор записи. |
PropertyObjectId | Guid | Да | Favorite.PropertyObjectId | — | Объект недвижимости для позиции подборки. |
CreatedAt | DateTimeOffset | Да | Favorite.CreatedAt | — | Дата добавления (порядок позиций в подборке по умолчанию). |
Note исключена намеренно (masking = exclude): подборка открывается по ссылке кем угодно,
личная заметка не должна иметь шанса утечь в shared-контент; selections собственные
комментарии позиций ведёт сам (../../../selections/domain/selection.md).
Алгоритм
Тривиальное чтение. Актор — m2m-клиент pin-selections (client credentials, проверено на gateway
internal-области). Проверяется вход: UserId != Guid.Empty, Limit ∈ 1..1000; существование пользователя
в identity не проверяется (кросс-контекстная ссылка без sync-вызова — несуществующий userId даст
пустой список). Читаются записи избранного пользователя (строго UserId = userId, гостевые записи
предикатом не затрагиваются), отсортированные по CreatedAt (новые сверху), не более Limit; параллельно
— общее число записей для TotalCount. Поле Note в проекцию не включается вовсе (masking = exclude).
Возвращается модель InternalFavoritesListResult; пустой список — валидный 200. Freshness: source of
truth, read-your-writes относительно операций пользователя.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
Unauthorized | 401 | Невалидный/чужой m2m-токен | стандартный ответ | no retry (конфигурация клиента) |
Forbidden | 403 | Клиент не в allowlist internal-области | стандартный ответ | no retry |
ValidationError | 400 | userId = Guid.Empty; limit ∉ 1..1000 | детали валидации | no retry |
InternalError | 500 | Отказ хранилища | generic message | retry на стороне selections (создание подборки откладывается) |
Acceptance-критерии
- Given m2m-клиент
pin-selections и userId с N записями, When
GET /internal/favorites?userId={userId}, Then ответ 200, Items (только PropertyObjectId +
даты, CreatedAt desc), TotalCount = N, поле Note отсутствует в каждом элементе.
- Given
userId без записей (или несуществующий пользователь), When GET, Then ответ 200,
Items = [], TotalCount = 0 (кросс-контекстное существование не проверяется).
- Given у пользователя записей больше
Limit, When GET, Then возвращается Limit элементов,
TotalCount отражает полное число (читатель selections предупреждает об усечении подборки).
- Given у пользователя есть гостевые записи с тем же логическим субъектом, When
GET, Then они
в выборку не попадают — читаются строго UserId-записи.
- Given клиент не в allowlist internal-области, When
GET, Then отказ Forbidden (403);
Given невалидный/чужой m2m-токен — Unauthorized (401); Given userId = Guid.Empty или
limit ∉ 1..1000 — ValidationError (400).
Совместимость и наблюдаемость
- Контракт internal, версионируется совместно с клиентом
selections; расширение — только additive
(Note не добавлять — правило безопасности выше); смена маршрута/полей — синхронный rollout обоих
сервисов.
- Logs: debug с
userId и счётчиком; списки PropertyObjectId не логируются.
- Metrics:
favorites_internal_list_total{client}, latency histogram.
- Traces: сквозной correlation от операции
selections.
- Dashboard/alerts/runbook — ../../README.md.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы