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

GET /api/v1/favorites — Список избранного владельца

Возвращает избранные объекты недвижимости текущего владельца (авторизованный пользователь или гостевая сессия) с личными заметками, отсортированные по дате добавления (новые сверху). Display-данные объектов недвижимости (цена/название/фото) фронт добирает батч-запросом в catalog по возвращённому списку PropertyObjectId — этот сервис объекты недвижимости не хранит и не реплицирует (../../domain/favorite.md, связь Favorite → PropertyObject).

Назначение

Страница избранного показывает пользователю его сохранённые объекты недвижимости с личными заметками; избранное доступно со всех устройств владельца. Гость получает список своей сессии без авторизации. Сервис отдаёт идентификаторы объектов недвижимости и заметки; визуальные карточки фронт добирает из catalog.

Metadata

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

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

ПроверкаЗначение
Actor typeAnonymous (гость по X-Anonymous-Id) или User (JWT)
Auth policy / Permissionsnone (self-scope); при наличии JWT приоритет у него
Scope (RBAC)не применяется — только собственные данные
Record-levelUserId = actor.UserId либо AnonymousId = X-Anonymous-Id (INV-FAV-005)
Tenant isolationне применяется — сущности вне tenant (ADR-0052 п.2); record-level фильтр обязателен
Auditnone (чтение несенситивных собственных данных; заметки видит только владелец)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/favorites
Success status200
Idempotency headerне применяется (безопасное чтение)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity (JWT claim sub)Guid?Условнозаполнен, если запрос авторизованприоритет над AnonymousId
AnonymousIdheader X-Anonymous-IdGuid?Условновалидный Guid ≠ Guid.Empty; используется только при отсутствии JWTнедоверенный вход — чтение ограничено записями этой сессии
Offsetquery offsetintНет (default 0)>= 0
Limitquery limitintНет (default 50)1..200защита от выгрузки сверхбольших страниц

Хотя бы один владелец (UserId или AnonymousId) обязан быть разрешён — иначе FAVORITES_OWNER_UNRESOLVED.

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

ПолеТипОбязательностьИсточникMaskingОписание
ItemsIReadOnlyList<FavoriteItemModel>Да (может быть пуст)projection FavoriteСтраница списка, сортировка CreatedAt desc.
TotalCountintДаcount по владельцуВсего записей владельца (для пагинации фронта).
OffsetintДаecho запросаПрименённый offset.
LimitintДаecho запросаПрименённый limit.

FavoriteItemModel

ПолеТипОбязательностьИсточникMaskingОписание
FavoriteIdGuidДаFavorite.IdИдентификатор записи избранного.
PropertyObjectIdGuidДаFavorite.PropertyObjectIdОбъект недвижимости; display-данные фронт добирает из catalog батчем.
Notestring?НетFavorite.Noteвидна только владельцу (self-scope запроса)Личная заметка ≤ 4000 символов (../../domain/favorite.md, INV-FAV-003).
CreatedAtDateTimeOffsetДаFavorite.CreatedAtДата добавления (ключ сортировки).
UpdatedAtDateTimeOffsetДаFavorite.UpdatedAtДата последнего изменения (правка заметки, слияние).

Алгоритм

  1. Контекст и доступ. Разрешить владельца: UserId из JWT, при его отсутствии — гостевая сессия AnonymousId из заголовка X-Anonymous-Id. Оба отсутствуют → отказ FAVORITES_OWNER_UNRESOLVED.
  2. Проверка входа. Offset >= 0, Limit ∈ 1..200; при гостевом запросе AnonymousId != Guid.Empty. Существование объектов недвижимости не проверяется (INV-FAV-004) — список может быть пустым.
  3. Что читаем. Записи избранного владельца, отсортированные по CreatedAt (новые сверху), страница по (Offset, Limit); параллельно — общее число записей владельца для TotalCount. Фильтр строго по владельцу (INV-FAV-005). Проекция сразу в FavoriteItemModel.
  4. Маппинг. FavoriteId ← Favorite.Id, PropertyObjectId ← Favorite.PropertyObjectId, Note ← Favorite.Note, CreatedAt/UpdatedAt — из записи; TotalCount ← count, Offset/Limit — echo входа. Фильтрация недоступных/скрытых объектов недвижимости здесь не выполняется — её делает catalog при батч-запросе фронта.
  5. Результат. Модель FavoritesListResult; Note = null, если заметка не задана; пустой список — валидный 200 с Items = []. Freshness: read-your-writes — читаем source of truth, реплик/кэшей нет (ADR-0056). HTTP 200 OK.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
FAVORITES_OWNER_UNRESOLVED400Нет ни JWT, ни валидного X-Anonymous-Id«Не удалось определить владельца избранного»no retry (фронт должен получить anonymous_id у user-activity)
ValidationError400Offset < 0, Limit ∉ 1..200, AnonymousId = Guid.Emptyдетали полейno retry
InternalError500Отказ хранилищаgeneric messageretry с backoff; фронт деградирует до кэша последнего ответа

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

  • Предусловие. Разрешён владелец (JWT или гость по X-Anonymous-Id); Offset >= 0, Limit ∈ 1..200.
  • Постусловие. Состояние не меняется (безопасное чтение); возвращается страница записей строго этого владельца (INV-FAV-005), сортировка CreatedAt desc, с корректным TotalCount для пагинации.

Acceptance-критерии

  • Given у владельца N записей, When GET /favorites?offset=0&limit=50, Then ответ 200, Items отсортированы CreatedAt desc, TotalCount = N, Offset/Limit — эхо запроса.
  • Given offset >= TotalCount, When GET, Then ответ 200 с Items = [] и корректным TotalCount (страница за концом набора — валидна).
  • Given у владельца записей нет, When GET, Then ответ 200, Items = [], TotalCount = 0.
  • Given запись владельца с непустой Note, When GET этим владельцем, Then Note возвращается; чужому владельцу запись недоступна (в выборку не попадает).
  • Given гость с X-Anonymous-Id, When GET, Then возвращаются только записи этой сессии, аккаунтные записи не видны.
  • Given Limit ∉ 1..200 или Offset < 0, When GET, Then отказ ValidationError (400).
  • Given запрос без JWT и без валидного X-Anonymous-Id, When GET, Then отказ FAVORITES_OWNER_UNRESOLVED (400).

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

  • Новые optional поля в FavoriteItemModel — additive, без breaking change; удаление/переименование/смена типа — breaking (migration + rollout по правилам совместимости).
  • Logs: debug-уровень с owner-типом (user/guest) и счётчиком записей; Note и списки PropertyObjectId не логируются.
  • Metrics: favorites_list_requests_total{owner_type}, latency histogram (SLO p99 < 150 мс — README).
  • Traces: сквозной correlation через traceparent.
  • Dashboard/alerts/runbook — ../../README.md (Runtime И Эксплуатация).

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

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

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