GET /api/v1/projects — Лента проектов
Назначение
Лента карточек проектов (ЖК и апарт-отели) для главной страницы и /hotels: карточки с минимальным
набором — название, адрес, девелопер, диапазон цен, количество объектов, окупаемость; полнотекстовый
поиск по названию/адресу/девелоперу, фильтры тип/класс/регион/город/статус строительства/ценовой диапазон,
сортировки цена/дата/популярность/окупаемость и дефолт «Рекомендованные», бесконечная подгрузка по
20–30 карточек. Доступ без гейта: анонимный гость видит ленту с урезанными полями — точные цены и
статусы объектов скрыты (правила MASK-PRICE/MASK-STATUS — README.md). Источник —
витрина catalog-project-card.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | catalog |
| API area | public |
| Feature group | feed |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Guest (маскированная лента) / RefGuest, User (полные поля) |
| Auth policy / Permissions | none (public read; маскирование вместо отказа) |
| Scope | платформа |
| Tenant isolation | не применяется (платформенная витрина) |
| Audit | none; поиск/фильтры — фронт пишет в user-activity |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/projects |
| Success status | 200 |
| Idempotency | не применяется (safe GET) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorClass | identity middleware | Guest/RefGuest/User | Да | невалидный токен → 401; Guest допустим | README.md |
SearchTerm | query | string? | Нет | trim; 1..256; передаётся параметром в полнотекстовый поиск (не конкатенация) | untrusted; лог — 64 симв. |
Kind | query | ProjectKind? | Нет | enum ResidentialComplex/ApartHotel (лента /hotels — Kind=ApartHotel); иное → ValidationError | — |
PropertyClass | query | PropertyClass? | Нет | enum Comfort/Business/Premium/Elite | — |
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-граница = открытый интервал; сопоставляется с диапазоном цен проекта (пересечение отрезков) | — |
Sort | query | FeedSort | Нет | enum (README.md); default Recommended; Price = по MinPrice, Payback = по Payback проекта | — |
Desc | query | bool? | Нет | default по сортировке; Recommended — фикс. desc | — |
Limit | query | int | Нет | 1..30, default 24 (лента 20–30) | — |
Cursor | query | string? | Нет | base64url FeedCursorModel (README.md) | не секрет |
Модель ответа GetProjectsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
items | IReadOnlyList<ProjectListItemModel> | Да | проекция витрины проектов | см. item | Страница карточек проектов. |
nextCursor | string? | Нет | computed | — | Курсор следующей страницы; null — конец ленты. |
totalCount | long | Да | счётчик по фильтру | — | Всего проектов под фильтром. |
appliedFilters | AppliedProjectFiltersModel | Да | echo запроса | — | Echo для URL-состояния (шаринг). |
ProjectListItemModel (минимальный набор ленты; источники — дом ../domain/catalog-project-card.md)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | CatalogProjectCard.Id | — | Проект (= id в chessboard). |
name | string | Да | Name (effective) | — | Название комплекса/отеля. |
kind | string (enum ProjectKind) | Да | Kind | — | ЖК / апарт-отель. |
propertyClass | string (enum PropertyClass) | Да | PropertyClass | — | Класс. |
regionId / regionName | long / string | Да | RegionId/RegionName | — | Регион (id справочника handbook + display-имя; у Published-проектов гео резолвлено — INV-PRJ-3 источника). |
cityId / cityName | long / string | Да | CityId/CityName | — | Город (id справочника handbook + display-имя). |
address | string | Да | Address (effective) | — | Адрес. |
developer | DeveloperRefModel | Да | DeveloperId/DeveloperName/DeveloperLogoFileId | — | Девелопер (ref-модель — README.md; лого — пресайн storage/CDN). |
coverFileId | Guid? | Нет | CoverFileId | — | Обложка карточки. |
shortDescription | string? | Нет | ShortDescription | — | Подсказка при наведении. |
constructionStatus | string (enum ConstructionStatus) | Да | ConstructionStatus | — | Статус строительства. |
deliveryDate | string (ISO 8601 date)? | Нет | DeliveryDate (effective) | — | Срок сдачи. |
readinessPercent | short? | Нет | ReadinessPercent (effective) | — | Готовность. |
stars | short? | Нет | Stars (effective) | — | Звёздность отеля. |
distanceMinutes | int? | Нет | DistanceMinutes (effective) | — | Минуты до ключевой точки. |
priceRange | NullableRange<decimal> | Да | MinPrice/MaxPrice | MASK-PRICE (Guest: min↓/max↑ до 100 000 ₽) | Диапазон цен ленты (ref-модель — README.md). |
pricePerMeterFrom | decimal? | Нет | зарезервировано null в v1 | MASK-PRICE (Guest → null) | Задел под «цена от … ₽/м²» (additive, появится после расширения агрегатов проекции). |
propertyObjectsTotal | int | Да | PropertyObjectsTotal | — | Всего объектов (количество объектов ленты). |
propertyObjectsAvailable | int? | Нет | PropertyObjectsAvailable | MASK-STATUS (Guest → null) | Доступные объекты (PropertyObjectStatus = Free). |
payback | PaybackModel? | Нет | owned Payback | — | Окупаемость проекта (мин. набор ленты — видна и гостю). |
growthPercent | decimal? | Нет | GrowthPercent | — | Рост цены, %/год. |
rating | decimal? | Нет | Rating | — | Рейтинг проекта. |
geoLocation | GeoLocationModel? | Нет | owned GeoLocation | — | Координаты для карты. |
AppliedProjectFiltersModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
searchTerm | string? | Нет | echo | — | — |
kind | string? | Нет | echo | — | — |
propertyClass | string? | Нет | echo | — | — |
regionId | long? | Нет | echo | — | — |
cityId | long? | Нет | echo | — | — |
constructionStatus | string? | Нет | echo | — | — |
price | NullableRange<decimal> | Нет | echo | — | Применённый ценовой диапазон (сериализуется в query как priceMin/priceMax). |
sort | string (enum FeedSort) | Да | echo (после default-ов) | — | — |
desc | bool | Да | echo (после default-ов) | — | — |
Алгоритм
- Контекст и доступ. Актор определяется по токену (
User; невалидный → 401), затем по реф-cookiereferralUrl(RefGuest), иначеGuest— лента доступна, маскирование вместо гейта. - Проверка входа. Enum-ы/диапазоны/паттерны по таблице входа,
Pricewell-formed (обе границы заданы →Price.Min <= Price.Max),Limit ∈ 1..30— иначеValidationError(400). Курсор проверяется как в get-property-objects.md шаг 2 → битый/несовместимый курсор даётCATALOG_CURSOR_INVALID(400). Отбор — жёстко по опубликованным (Status = Published). - Что читаем. Страница карточек проектов под фильтром:
SearchTerm— полнотекстовый поиск по названию/адресу/девелоперу; фильтрыKind/PropertyClass/RegionId/CityId/ConstructionStatus; ценовой диапазон — пересечение отрезка запроса с диапазоном цен проекта; keyset-предикат из курсора и сортировка по выбранному ключу (по умолчанию — «Рекомендованные»). Отдельно — общий счётчик под фильтром без keyset. - Сборка ответа и маскирование. Маппинг полей — таблица
ProjectListItemModel. ДляGuest: MASK-PRICE (priceRangeокругляется вниз/вверх до 100 000 ₽, приnullостаётсяnull) и MASK-STATUS (propertyObjectsAvailable = null). Дефолт без явного выбора —Recommended(формула ранжирования, финальный ключ — дата публикации).nextCursor— из последней строки, если выбран лишний элемент сверх лимита. - Результат.
GetProjectsQueryResult→ 200;appliedFilters— источник URL-состояния. Консистентность — eventual (агрегатыMinPrice/PropertyObjectsAvailableпересчитываются консюмерами — INV-2 project-card).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Передан невалидный/просроченный токен | без тела схемы | re-login |
ValidationError | 400 | Нарушение ограничений входа | Список ошибок полей | no retry |
CATALOG_CURSOR_INVALID | 400 | Битый/несовместимый курсор | «Курсор устарел — обновите ленту» | retry без cursor |
Пустая выдача — не ошибка
200 с items = [] — валидный ответ.
Acceptance Criteria
- Лента гостю. Дано
Guestи опубликованные проекты. Когда запрашивает/api/v1/projects. Тогда200, карточки с округлённымpriceRange(MASK-PRICE) иpropertyObjectsAvailable = null(MASK-STATUS); гейта нет. - Дефолт «Рекомендованные». Дано
Sortне задан. Когда формируется страница. Тогда сортировкаRecommended(формула ранжирования, финальный ключ — дата публикации), направление фиксированоdesc. - Поиск и фильтры. Дано
searchTerm+kind=ApartHotel+ ценовой диапазон. Когда выполняется запрос. Тогда возвращаются толькоApartHotelс пересечением ценового отрезка;totalCount— под фильтром. - Фильтр «регион»/«город». Дано
regionId/cityIdиз lookup-а фильтров. Когда выполняется запрос. Тогда возвращаются только проекты с этимRegionId/CityId; несуществующий id даёт200сitems = [](не ошибка);regionId <= 0илиcityId <= 0→ValidationError(400). - Пагинация. Дано первая страница вернула
nextCursor. Когда клиент запрашивает соcursor. Тогда следующая страница без дублей в пределах той же сортировки; конец ленты →nextCursor = null. - Битый/несовместимый курсор. Дано
cursorот другой сортировки или версии. Когда приходит запрос. ТогдаCATALOG_CURSOR_INVALID(400); клиент повторяет безcursor(первая страница). - Стабильность при переранжировании. Дано во время подгрузки консюмер/джоб изменил
PromotionBoost/PopularityScore/DeveloperPriorityэлемента. Когда клиент листаетRecommended. Тогда keyset исключает дубли при неизменных ключах, но сдвиг ранга элемента между страницами может привести к его пропуску/повторному показу — принятое ограничение бесконечной ленты (обновление ленты снимает расхождение). - Пустая выдача. Дано фильтр без совпадений. Когда запрос выполнен. Тогда
200сitems = [].
Совместимость и наблюдаемость
- Новые optional-поля item — additive (
pricePerMeterFromзаложенnull-ом); раскрытие маскированных полей гостю — breaking (MASK-* в README.md). - Logs/metrics/traces — общие правила area; дополнительно разрез метрик по
kind(ЖК vs отели) иactor_class(доля маскированных ответов).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Catalog Public API — реестр методов и общие правила, GET /api/v1/catalog/filters — Справочники значений фильтров, GET /api/v1/projects/{id} — Карточка проекта: ограниченная / полная, GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки)
- Фоновые задачи (1): Background Job: PopularityRecomputeJob
- registries (1): Реестр API-методов PIN
Связанные документы
- Общие правила area: README.md; дом полей:
../domain/catalog-project-card.md; формула
Recommended: ../domain/ranking-rule.md. - Карточка проекта: get-project.md; ассортимент: get-project-property-objects.md; lookup фильтров: get-catalog-filters.md.
- Трекинг:
../../user-activity/.