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

GET /api/v1/lots — Лента объектов недвижимости

Назначение

Бесконечная лента карточек объектов недвижимости страницы /lots: полнотекстовый поиск по названию/адресу/девелоперу, фильтры тип/класс/регион/город/статус строительства/цена/комнатность, сортировки цена/дата/популярность/окупаемость и дефолтная «Рекомендованные» (ранжирование застройщика + пакеты продвижения), показ комиссии агенту в списке. Источник — одна витрина catalog-property-object-card (денормализованная проекция: лента отвечает одной строкой без джойнов); фильтры уровня проекта разрешаются по витрине проектов. Общие правила area (гейт, маскирование, курсор, сортировки, общие модели) — README.md.

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areapublic
Feature groupfeed
Статусdraft

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

ПроверкаЗначение
Actor typeUser или RefGuest (GATE-FEED, README.md); Guest → 401
Auth policy / Permissionsчтение — без permission (гейт вместо него); поле agentCommission — только при catalog.commissions.view (MASK-COMMISSION)
Scopeплатформа (витрина публична в рамках гейта)
Tenant isolationне применяется — платформенная витрина
Auditnone (public read); история поиска/фильтров — фронт пишет в user-activity

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/lots
Success status200
Idempotencyне применяется (safe GET)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorClassidentity middlewareUser/RefGuestДаGATE-FEED: иначе 401классы — README.md
SearchTermquerystring?Нетtrim; 1..256 символов; передаётся параметром в полнотекстовый поиск (не конкатенация)untrusted input; в логи — первые 64 симв.
ProjectIdqueryGuid?Нетвалидный Guid
TypeNamequerystring?Нет1..64; значение из lookup get-catalog-filters.md (несуществующее — пустая выдача, не ошибка)
PropertyClassqueryPropertyClass?Нетenum Comfort/Business/Premium/Elite (дом — ../domain/catalog-project-card.md); иное → ValidationError
RegionIdquerylong?Нет> 0; ключ фильтра «регион» — ссылка на справочник handbook (значения — lookup get-catalog-filters.md); несуществующий id — пустая выдача, не ошибка
CityIdquerylong?Нет> 0; ключ фильтра «город» (значения — тот же lookup); несуществующий id — пустая выдача
ConstructionStatusqueryConstructionStatus?Нетenum Planned/InProgress/Commissioned
Pricequery (priceMin/priceMaxPrice.Min/Price.Max)NullableRange<decimal>Нетзаданные границы > 0; well-formed Price.Min <= Price.Max; null-граница = открытый интервал; NullableRange.Any — фильтр не задан
Roomsquery (csv, напр. rooms=0,1,2)IReadOnlyList<short>?Неткаждый 0..20 (0 = студия); максимум 10 значений
SortqueryFeedSortНетenum (README.md); default Recommended
Descquerybool?Нетdefault по сортировке (README.md); для Recommended игнорируется (фикс. desc)
LimitqueryintНет1..30, default 24 (лента 20–30)
Cursorquerystring?Нетbase64url FeedCursorModel (README.md): ContractVersion=1, Sort/Descending совпадают с query, обязательные Last* заполнены → иначе CATALOG_CURSOR_INVALIDне секрет; подмена = другая страница

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

ПолеТипОбязательностьИсточникMaskingОписание
itemsIReadOnlyList<PropertyObjectListItemModel>Дапроекция объектов витринысм. itemСтраница карточек.
nextCursorstring?НетcomputedКурсор следующей страницы; null — лента исчерпана.
totalCountlongДасчётчик по фильтруВсего карточек под фильтром (счётчик «найдено N объектов»).
appliedFiltersAppliedPropertyObjectFiltersModelДаecho запросаEcho применённых фильтров для синхронизации URL (шаринг).

PropertyObjectListItemModel (источники полей — дом ../domain/catalog-property-object-card.md)

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаCatalogPropertyObjectCard.IdОбъект недвижимости (= id в chessboard).
projectIdGuidДаProjectIdПроект для перехода в карточку проекта.
numberstringДаNumberНомер объекта недвижимости.
titlestringДаTitle (effective)Заголовок карточки.
typeNamestringДаTypeNameТип объекта.
roomsCountshortДаRoomsCountКомнатность (0 = студия).
viewstring?НетViewВид из окон.
buildingBuildingRefModelДаBuildingId/BuildingNameКорпус (ref-модель — README.md).
badgestring?НетBadge (effective)Маркетинговый бейдж.
deliveryDatestring (ISO 8601 date)?НетDeliveryDate (effective)Срок сдачи.
starsshort?НетStars (effective)Звёздность отеля.
distanceMinutesint?НетDistanceMinutes (effective)Минуты до ключевой точки.
addressstringДаAddressАдрес проекта.
developerDeveloperRefModelДаDeveloperId/DeveloperNameЗастройщик (ref-модель — README.md).
pricedecimalДаPrice (effective)Цена (актор прошёл гейт — маскирование цены не применяется).
areaTotaldecimalДаAreaTotalОбщая площадь, м².
pricePerMeterdecimalДаPricePerMeterЦена за м².
readinessPercentshort?НетReadinessPercent (effective)Готовность строительства.
propertyObjectStatusstring (enum PropertyObjectAvailabilityStatus)ДаPropertyObjectStatusFree/Reserved/Booked/SoldPublished-выдаче Hidden не встречается — INV-1).
paybackPaybackModel?Нетowned Payback (effective)Окупаемость (README.md).
reliabilityReliabilityModel?Нетowned Reliability (effective)Индекс надёжности.
growthPercentdecimal?НетGrowthPercentПрогнозный рост цены, %/год.
rentalIncomePerYeardecimal?НетRentalIncomePerYearАрендный доход, ₽/год.
ratingdecimal?НетRating (effective)Рейтинг объекта.
visualizationFileIdGuid?НетVisualizationFileIdФайл визуализации (пресайн — граница storage/CDN).
agentCommissionAgentCommissionModel?Нетowned AgentCommissionMASK-COMMISSIONКомиссия агента; null — нет permission / флаг выключен / комиссия не задана.

AppliedPropertyObjectFiltersModel

ПолеТипОбязательностьИсточникMaskingОписание
searchTermstring?НетechoПрименённый поиск.
projectIdGuid?Нетecho
typeNamestring?Нетecho
propertyClassstring?Нетecho
regionIdlong?Нетecho
cityIdlong?Нетecho
constructionStatusstring?Нетecho
priceNullableRange<decimal>НетechoПрименённый ценовой диапазон (сериализуется в query как priceMin/priceMax).
roomsIReadOnlyList<short>?Нетecho
sortstring (enum FeedSort)Даecho (после default-ов)Итоговая сортировка.
descboolДаecho (после default-ов)Итоговое направление.

Алгоритм

  1. Контекст и доступ. Актор по токену (User; невалидный → 401), иначе по реф-cookie referralUrl (RefGuest), иначе Guest401 (GATE-FEED). Для User из прав определяется наличие catalog.commissions.view.
  2. Проверка входа. Диапазоны/enum-ы/паттерны по таблице входа, Price well-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).
  3. Что читаем. Страница карточек объектов под фильтром: SearchTerm — полнотекстовый поиск по названию/адресу/девелоперу; собственные фильтры объекта (ProjectId, TypeName, Rooms, Price); фильтры уровня проекта (PropertyClass/RegionId/CityId/ConstructionStatus) проверяются по витрине проектов; keyset-предикат из курсора и сортировка по выбранному ключу (по умолчанию — «Рекомендованные»). Отдельно — общий счётчик под тем же фильтром без keyset.
  4. Сборка ответа. Маппинг поле-в-поле — таблица PropertyObjectListItemModel. payback/reliability/agentCommissionnull, если значение не заполнено; agentCommission дополнительно обнуляется по MASK-COMMISSION (нет права catalog.commissions.view или флаг catalog.commissions.enabled = false); deliveryDate — ISO-дата. nextCursor собирается из последней строки, если выбран лишний элемент сверх лимита.
  5. Результат. GetPropertyObjectsQueryResult → 200: items (с масками), nextCursor, totalCount, appliedFilters (echo после default-ов — источник состояния URL). Консистентность: eventual (свежесть проекции — лаг консюмеров, SLO ../README.md); внутри одной сортировки keyset даёт стабильную ленту без дублей при подгрузке.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Guest (нет токена и нет реф-cookie) либо невалидный токен«Требуется вход или реферальная ссылка»после входа
ValidationError400Нарушение ограничений входных данныхСписок ошибок полейno retry
CATALOG_CURSOR_INVALID400Курсор не парсится / не соответствует 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, itemsPublished объекты под фильтром, 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 размера выдачи.

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

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

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