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

GET /internal/favorites — Чтение избранного пользователя (m2m для selections)

Internal-операция: контекст selections читает текущее избранное пользователя при создании подборки «из текущего избранного» (см. ../../../selections/README.md). Возвращает только PropertyObjectId и даты — личные заметки (Note) не отдаются ни при каких условиях: заметка приватна для владельца и не покидает контекст favorites (../../domain/favorite.md, раздел безопасности).

Назначение

Единственный внешний читатель — сервис selections (m2m). Вызов происходит в рамках операции создания подборки, инициированной самим пользователем-владельцем избранного; selections передаёт userId актора своей команды. Гостевое избранное (AnonymousId) через этот endpoint не читается — подборку может создать только авторизованный пользователь (владелец подборки обязателен).

Metadata

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

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

ПроверкаЗначение
Actor typeSystem (m2m: сервис selections)
Auth policy / Permissionsm2m 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)
Auditnone (чтение без sensitive-полей — заметки исключены из ответа)

HTTP-контракт

ПолеЗначение
MethodGET
Route/internal/favorites?userId={userId}
Success status200
Idempotency headerне применяется (безопасное чтение)
Correlationсквозной trace через заголовок traceparent из операции selections

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdquery userIdGuidДа!= Guid.Emptyдоверенный вход из m2m-клиента; сопоставление с актором операции — обязанность selections
Limitquery limitintНет (default 500)1..1000ограничение размера подборки на стороне читателя

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

ПолеТипОбязательностьИсточникMaskingОписание
ItemsIReadOnlyList<InternalFavoriteItemModel>Да (может быть пуст)projection FavoriteИзбранное пользователя, CreatedAt desc.
TotalCountintДаcount по UserIdВсего записей (если больше Limit — фронт selections предупредит об усечении).

InternalFavoriteItemModel

ПолеТипОбязательностьИсточникMaskingОписание
FavoriteIdGuidДаFavorite.IdИдентификатор записи.
PropertyObjectIdGuidДаFavorite.PropertyObjectIdОбъект недвижимости для позиции подборки.
CreatedAtDateTimeOffsetДа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
Unauthorized401Невалидный/чужой m2m-токенстандартный ответno retry (конфигурация клиента)
Forbidden403Клиент не в allowlist internal-областистандартный ответno retry
ValidationError400userId = Guid.Empty; limit ∉ 1..1000детали валидацииno retry
InternalError500Отказ хранилищаgeneric messageretry на стороне 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..1000ValidationError (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.

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

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

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

  • Потребитель: ../../../selections/README.md и будущая операция docs/06-services/selections/api/public/create-selection.md (создание подборки из текущего избранного).
  • Сущность: ../../domain/favorite.md (приватность Note, INV-FAV-005).