GET /api/v1/favorites — Список избранного владельца
Возвращает избранные объекты недвижимости текущего владельца (авторизованный пользователь или гостевая сессия) с личными
заметками, отсортированные по дате добавления (новые сверху). Display-данные объектов недвижимости (цена/название/фото)
фронт добирает батч-запросом в catalog по возвращённому списку PropertyObjectId — этот сервис объекты недвижимости не хранит
и не реплицирует (../../domain/favorite.md, связь Favorite → PropertyObject).
Назначение
Страница избранного показывает пользователю его сохранённые объекты недвижимости с личными заметками;
избранное доступно со всех устройств владельца. Гость получает список своей сессии без авторизации.
Сервис отдаёт идентификаторы объектов недвижимости и заметки; визуальные карточки фронт добирает из catalog.
| Поле | Значение |
|---|
| Сервис/контекст | favorites |
| API area | public |
| Feature group | favorites |
| Статус | draft |
| Документ | docs/06-services/favorites/api/public/get-my-favorites.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Anonymous (гость по X-Anonymous-Id) или User (JWT) |
| Auth policy / Permissions | none (self-scope); при наличии JWT приоритет у него |
| Scope (RBAC) | не применяется — только собственные данные |
| Record-level | UserId = actor.UserId либо AnonymousId = X-Anonymous-Id (INV-FAV-005) |
| Tenant isolation | не применяется — сущности вне tenant (ADR-0052 п.2); record-level фильтр обязателен |
| Audit | none (чтение несенситивных собственных данных; заметки видит только владелец) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/favorites |
| Success status | 200 |
| Idempotency header | не применяется (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity (JWT claim sub) | Guid? | Условно | заполнен, если запрос авторизован | приоритет над AnonymousId |
AnonymousId | header X-Anonymous-Id | Guid? | Условно | валидный Guid ≠ Guid.Empty; используется только при отсутствии JWT | недоверенный вход — чтение ограничено записями этой сессии |
Offset | query offset | int | Нет (default 0) | >= 0 | — |
Limit | query limit | int | Нет (default 50) | 1..200 | защита от выгрузки сверхбольших страниц |
Хотя бы один владелец (UserId или AnonymousId) обязан быть разрешён — иначе FAVORITES_OWNER_UNRESOLVED.
Модель ответа FavoritesListResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Items | IReadOnlyList<FavoriteItemModel> | Да (может быть пуст) | projection Favorite | — | Страница списка, сортировка CreatedAt desc. |
TotalCount | int | Да | count по владельцу | — | Всего записей владельца (для пагинации фронта). |
Offset | int | Да | echo запроса | — | Применённый offset. |
Limit | int | Да | echo запроса | — | Применённый limit. |
FavoriteItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
FavoriteId | Guid | Да | Favorite.Id | — | Идентификатор записи избранного. |
PropertyObjectId | Guid | Да | Favorite.PropertyObjectId | — | Объект недвижимости; display-данные фронт добирает из catalog батчем. |
Note | string? | Нет | Favorite.Note | видна только владельцу (self-scope запроса) | Личная заметка ≤ 4000 символов (../../domain/favorite.md, INV-FAV-003). |
CreatedAt | DateTimeOffset | Да | Favorite.CreatedAt | — | Дата добавления (ключ сортировки). |
UpdatedAt | DateTimeOffset | Да | Favorite.UpdatedAt | — | Дата последнего изменения (правка заметки, слияние). |
Алгоритм
- Контекст и доступ. Разрешить владельца:
UserId из JWT, при его отсутствии — гостевая сессия
AnonymousId из заголовка X-Anonymous-Id. Оба отсутствуют → отказ FAVORITES_OWNER_UNRESOLVED.
- Проверка входа.
Offset >= 0, Limit ∈ 1..200; при гостевом запросе AnonymousId != Guid.Empty.
Существование объектов недвижимости не проверяется (INV-FAV-004) — список может быть пустым.
- Что читаем. Записи избранного владельца, отсортированные по
CreatedAt (новые сверху), страница по
(Offset, Limit); параллельно — общее число записей владельца для TotalCount. Фильтр строго по
владельцу (INV-FAV-005). Проекция сразу в FavoriteItemModel.
- Маппинг.
FavoriteId ← Favorite.Id, PropertyObjectId ← Favorite.PropertyObjectId,
Note ← Favorite.Note, CreatedAt/UpdatedAt — из записи; TotalCount ← count, Offset/Limit —
echo входа. Фильтрация недоступных/скрытых объектов недвижимости здесь не выполняется — её делает
catalog при батч-запросе фронта.
- Результат. Модель
FavoritesListResult; Note = null, если заметка не задана; пустой список —
валидный 200 с Items = []. Freshness: read-your-writes — читаем source of truth, реплик/кэшей нет
(ADR-0056). HTTP 200 OK.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
FAVORITES_OWNER_UNRESOLVED | 400 | Нет ни JWT, ни валидного X-Anonymous-Id | «Не удалось определить владельца избранного» | no retry (фронт должен получить anonymous_id у user-activity) |
ValidationError | 400 | Offset < 0, Limit ∉ 1..200, AnonymousId = Guid.Empty | детали полей | no retry |
InternalError | 500 | Отказ хранилища | generic message | retry с 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 И Эксплуатация).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы