GET /api/v1/lots — Лента объектов недвижимости
Назначение
Бесконечная лента карточек объектов недвижимости страницы /lots: полнотекстовый поиск по
названию/адресу/девелоперу, фильтры тип/класс/регион/город/статус строительства/цена/комнатность, сортировки
цена/дата/популярность/окупаемость и дефолтная «Рекомендованные» (ранжирование застройщика + пакеты
продвижения), показ комиссии агенту в списке. Источник — одна витрина
catalog-property-object-card (денормализованная
проекция: лента отвечает одной строкой без джойнов); фильтры уровня проекта разрешаются по витрине
проектов. Общие правила area (гейт, маскирование, курсор, сортировки, общие модели) —
README.md.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | catalog |
| API area | public |
| Feature group | feed |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User или RefGuest (GATE-FEED, README.md); Guest → 401 |
| Auth policy / Permissions | чтение — без permission (гейт вместо него); поле agentCommission — только при catalog.commissions.view (MASK-COMMISSION) |
| Scope | платформа (витрина публична в рамках гейта) |
| Tenant isolation | не применяется — платформенная витрина |
| Audit | none (public read); история поиска/фильтров — фронт пишет в user-activity |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/lots |
| Success status | 200 |
| Idempotency | не применяется (safe GET) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorClass | identity middleware | User/RefGuest | Да | GATE-FEED: иначе 401 | классы — README.md |
SearchTerm | query | string? | Нет | trim; 1..256 символов; передаётся параметром в полнотекстовый поиск (не конкатенация) | untrusted input; в логи — первые 64 симв. |
ProjectId | query | Guid? | Нет | валидный Guid | — |
TypeName | query | string? | Нет | 1..64; значение из lookup get-catalog-filters.md (несуществующее — пустая выдача, не ошибка) | — |
PropertyClass | query | PropertyClass? | Нет | enum Comfort/Business/Premium/Elite (дом — ../domain/catalog-project-card.md); иное → ValidationError | — |
RegionId | query | long? | Нет | > 0; ключ фильтра «регион» — ссылка на справочник handbook (значения — lookup get-catalog-filters.md); несуществующий id — пустая выдача, не ошибка | — |
CityId | query | long? | Нет | > 0; ключ фильтра «город» (значения — тот же lookup); несуществующий id — пустая выдача | — |
ConstructionStatus | query | ConstructionStatus? | Нет | enum Planned/InProgress/Commissioned | — |
Price | query (priceMin/priceMax → Price.Min/Price.Max) | NullableRange<decimal> | Нет | заданные границы > 0; well-formed Price.Min <= Price.Max; null-граница = открытый интервал; NullableRange.Any — фильтр не задан | — |
Rooms | query (csv, напр. rooms=0,1,2) | IReadOnlyList<short>? | Нет | каждый 0..20 (0 = студия); максимум 10 значений | — |
Sort | query | FeedSort | Нет | enum (README.md); default Recommended | — |
Desc | query | bool? | Нет | default по сортировке (README.md); для Recommended игнорируется (фикс. desc) | — |
Limit | query | int | Нет | 1..30, default 24 (лента 20–30) | — |
Cursor | query | string? | Нет | base64url FeedCursorModel (README.md): ContractVersion=1, Sort/Descending совпадают с query, обязательные Last* заполнены → иначе CATALOG_CURSOR_INVALID | не секрет; подмена = другая страница |
Модель ответа GetPropertyObjectsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
items | IReadOnlyList<PropertyObjectListItemModel> | Да | проекция объектов витрины | см. item | Страница карточек. |
nextCursor | string? | Нет | computed | — | Курсор следующей страницы; null — лента исчерпана. |
totalCount | long | Да | счётчик по фильтру | — | Всего карточек под фильтром (счётчик «найдено N объектов»). |
appliedFilters | AppliedPropertyObjectFiltersModel | Да | echo запроса | — | Echo применённых фильтров для синхронизации URL (шаринг). |
PropertyObjectListItemModel (источники полей — дом ../domain/catalog-property-object-card.md)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | CatalogPropertyObjectCard.Id | — | Объект недвижимости (= id в chessboard). |
projectId | Guid | Да | ProjectId | — | Проект для перехода в карточку проекта. |
number | string | Да | Number | — | Номер объекта недвижимости. |
title | string | Да | Title (effective) | — | Заголовок карточки. |
typeName | string | Да | TypeName | — | Тип объекта. |
roomsCount | short | Да | RoomsCount | — | Комнатность (0 = студия). |
view | string? | Нет | View | — | Вид из окон. |
building | BuildingRefModel | Да | BuildingId/BuildingName | — | Корпус (ref-модель — README.md). |
badge | string? | Нет | Badge (effective) | — | Маркетинговый бейдж. |
deliveryDate | string (ISO 8601 date)? | Нет | DeliveryDate (effective) | — | Срок сдачи. |
stars | short? | Нет | Stars (effective) | — | Звёздность отеля. |
distanceMinutes | int? | Нет | DistanceMinutes (effective) | — | Минуты до ключевой точки. |
address | string | Да | Address | — | Адрес проекта. |
developer | DeveloperRefModel | Да | DeveloperId/DeveloperName | — | Застройщик (ref-модель — README.md). |
price | decimal | Да | Price (effective) | — | Цена (актор прошёл гейт — маскирование цены не применяется). |
areaTotal | decimal | Да | AreaTotal | — | Общая площадь, м². |
pricePerMeter | decimal | Да | PricePerMeter | — | Цена за м². |
readinessPercent | short? | Нет | ReadinessPercent (effective) | — | Готовность строительства. |
propertyObjectStatus | string (enum PropertyObjectAvailabilityStatus) | Да | PropertyObjectStatus | — | Free/Reserved/Booked/Sold (в Published-выдаче Hidden не встречается — INV-1). |
payback | PaybackModel? | Нет | owned Payback (effective) | — | Окупаемость (README.md). |
reliability | ReliabilityModel? | Нет | owned Reliability (effective) | — | Индекс надёжности. |
growthPercent | decimal? | Нет | GrowthPercent | — | Прогнозный рост цены, %/год. |
rentalIncomePerYear | decimal? | Нет | RentalIncomePerYear | — | Арендный доход, ₽/год. |
rating | decimal? | Нет | Rating (effective) | — | Рейтинг объекта. |
visualizationFileId | Guid? | Нет | VisualizationFileId | — | Файл визуализации (пресайн — граница storage/CDN). |
agentCommission | AgentCommissionModel? | Нет | owned AgentCommission | MASK-COMMISSION | Комиссия агента; null — нет permission / флаг выключен / комиссия не задана. |
AppliedPropertyObjectFiltersModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
searchTerm | string? | Нет | echo | — | Применённый поиск. |
projectId | Guid? | Нет | echo | — | — |
typeName | string? | Нет | echo | — | — |
propertyClass | string? | Нет | echo | — | — |
regionId | long? | Нет | echo | — | — |
cityId | long? | Нет | echo | — | — |
constructionStatus | string? | Нет | echo | — | — |
price | NullableRange<decimal> | Нет | echo | — | Применённый ценовой диапазон (сериализуется в query как priceMin/priceMax). |
rooms | IReadOnlyList<short>? | Нет | echo | — | — |
sort | string (enum FeedSort) | Да | echo (после default-ов) | — | Итоговая сортировка. |
desc | bool | Да | echo (после default-ов) | — | Итоговое направление. |
Алгоритм
- Контекст и доступ. Актор по токену (
User; невалидный → 401), иначе по реф-cookiereferralUrl(RefGuest), иначеGuest→ 401 (GATE-FEED). ДляUserиз прав определяется наличиеcatalog.commissions.view. - Проверка входа. Диапазоны/enum-ы/паттерны по таблице входа,
Pricewell-formed (обе границы заданы →Price.Min <= Price.Max),Rooms.Count <= 10,Limit ∈ 1..30— иначеValidationError(400). Курсор: сбой парсинга,ContractVersion != 1, несоответствиеSort/Descили отсутствие обязательныхLast*-полей →CATALOG_CURSOR_INVALID(400).ProjectId— фильтр: несуществующий id даёт пустую выдачу (totalCount = 0), это валидный ответ. Отбор — жёстко по опубликованным (Status = Published). - Что читаем. Страница карточек объектов под фильтром:
SearchTerm— полнотекстовый поиск по названию/адресу/девелоперу; собственные фильтры объекта (ProjectId,TypeName,Rooms,Price); фильтры уровня проекта (PropertyClass/RegionId/CityId/ConstructionStatus) проверяются по витрине проектов; keyset-предикат из курсора и сортировка по выбранному ключу (по умолчанию — «Рекомендованные»). Отдельно — общий счётчик под тем же фильтром без keyset. - Сборка ответа. Маппинг поле-в-поле — таблица
PropertyObjectListItemModel.payback/reliability/agentCommission—null, если значение не заполнено;agentCommissionдополнительно обнуляется по MASK-COMMISSION (нет праваcatalog.commissions.viewили флагcatalog.commissions.enabled = false);deliveryDate— ISO-дата.nextCursorсобирается из последней строки, если выбран лишний элемент сверх лимита. - Результат.
GetPropertyObjectsQueryResult→ 200:items(с масками),nextCursor,totalCount,appliedFilters(echo после default-ов — источник состояния URL). Консистентность: eventual (свежесть проекции — лаг консюмеров, SLO ../README.md); внутри одной сортировки keyset даёт стабильную ленту без дублей при подгрузке.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Guest (нет токена и нет реф-cookie) либо невалидный токен | «Требуется вход или реферальная ссылка» | после входа |
ValidationError | 400 | Нарушение ограничений входных данных | Список ошибок полей | no retry |
CATALOG_CURSOR_INVALID | 400 | Курсор не парсится / не соответствует Sort/Desc / нет обязательных ключей / чужой ContractVersion | «Курсор устарел — обновите ленту» | retry без cursor (первая страница) |
Пустая выдача — не ошибка
200 с items = [], totalCount = 0 — валидный ответ (например, для несуществующего ProjectId).
Acceptance Criteria
- Гейт (гость). Дано
Guest(нет токена и реф-cookie). Когда запрашивает/api/v1/lots. Тогда401— лента объектов за GATE-FEED. - Happy path. Дано
User/RefGuestи опубликованные объекты. Когда запрашивает с фильтрамиrooms=1,2иsort=Recommended. Тогда200,items—Publishedобъекты под фильтром,priceбез маскирования,totalCount— «найдено N объектов». - Комиссия по праву. Дано право
catalog.commissions.viewотсутствует либо флагcatalog.commissions.enabled = false. Когда возвращаются карточки. ТогдаagentCommission = null(MASK-COMMISSION). - Битый/несовместимый курсор. Дано
cursorне парсится, либоSort/Desc/ContractVersionне совпали, либо нет обязательныхLast*. Когда приходит запрос. ТогдаCATALOG_CURSOR_INVALID(400); повтор безcursor— первая страница. - Несуществующий
ProjectId. Дано фильтрProjectIdбез карточек. Когда запрос выполнен. Тогда200сitems = [],totalCount = 0(не ошибка). - Стабильность подгрузки. Дано неизменные ключи сортировки. Когда клиент листает. Тогда keyset даёт
ленту без дублей; при асинхронном переранжировании (
PromotionBoost/PopularityScore) элемент может сдвинуться между страницами и оказаться пропущен/повторён — принятое ограничение бесконечной ленты.
Совместимость и наблюдаемость
- Новые optional-поля
PropertyObjectListItemModel— additive. Смена схемы курсора — breaking →ContractVersion(README.md). - Logs/metrics/traces — общие правила area (README.md); дополнительно метрика
catalog_property_objects_search_requests_total(доля запросов сsearchTerm) и histogram размера выдачи.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (6): Catalog Public API — реестр методов и общие правила, GET /api/internal/user-activity/property-objects/view-stats — Батч платформенных счётчиков просмотров объектов недвижимости, GET /api/v1/catalog/filters — Справочники значений фильтров, GET /api/v1/lots/{id} — Карточка объекта недвижимости: ограниченная / полная, GET /api/v1/projects — Лента проектов, GET /api/v1/projects/{id}/lots — Ассортимент проекта/отеля
- Фоновые задачи (1): Background Job: PopularityRecomputeJob
- registries (1): Реестр API-методов PIN
Связанные документы
- Общие правила area: README.md.
- Дом полей: ../domain/catalog-property-object-card.md; формула
Recommended: ../domain/ranking-rule.md; буст (PromotionBoost, консюмерpromotion): ../consumers/promotion-boost.md. - Карточка объекта недвижимости: get-property-object.md; lookup фильтров: get-catalog-filters.md.
- Трекинг поиска/фильтров:
../../user-activity/.