GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки)
Назначение
Публичная лента карточек проектов (ЖК / апарт-отелей) — первый экран каталога и страницы /hotels:
пагинация, полнотекстовый поиск по названию и адресу (Elasticsearch-проекция ProjectSearchDocument,
ADR-0059), фильтры «тип / класс / регион / город / статус строительства / ценовой диапазон» и сортировки
«цена / дата добавления / окупаемость». Фильтры «регион»/«город» — по ссылкам справочника
(RegionId/CityId, handbook-replica.md); display-имена в карточках —
денормализованные RegionName/CityName проекта. Owning-контекст — chessboard; лента строится поверх source of truth
chessboard.projects (структурная выборка).
Доступ без гейта: анонимный гость видит ленту с урезанными полями — точные цены скрыты. Зеркалируется в
agents area (GET /api/v1/agents/projects) той же операцией, но с обязательной авторизацией агента —
IsAuthenticated всегда true, masking отключён.
Ранжирование, пакеты продвижения и оверрайды отображения витрины — контекст catalog
(../../../../catalog/api/public/get-projects.md); здесь —
структурная лента поверх шахматки. Запись просмотров/поиска в историю — user-activity, не этот метод.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | public (зеркало: agents) |
| Feature group | projects |
| Статус | draft |
| Owner | PIN Platform Core |
| Документ | docs/06-services/chessboard/api/public/projects/search-projects.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous (ограниченная лента) / User — инвестор, агент, менеджер застройщика (полная лента) |
| Auth policy / Permissions | none — public endpoint; полнота полей определяется признаком авторизации |
| Scope (RBAC) | не применяется (платформенное чтение published-среза) |
| Record-level | не применяется |
| Tenant isolation | Контролируемый bypass tenant-фильтра только для среза Project.Status = Published && Developer.Status = Published (ADR-0052, INV-DEV-6); тот же срез применяется к полнотекстовому поиску |
| Audit | none (read; просмотры/поиск трекает user-activity) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/public/projects |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
IsAuthenticated | identity claims | bool | Да (вычисляется) | из контекста авторизации; аноним = false; agents-зеркало = всегда true | определяет masking |
ActorUserId | identity claims | Guid? | Нет | null для анонима | не логируется в теле ответа |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 20) | 1..100 | — |
Search | query | string? | Нет | <= 256 симв.; полнотекстовый поиск по Name + Address через Elasticsearch-проекцию ProjectSearchDocument (ADR-0059) | untrusted; передаётся только как поисковый терм (не raw-DSL); лог — 64 симв. |
Types | query (csv) | ProjectType[]? | Нет | ResidentialComplex/ApartHotel/Hotel/Villa (reference-data.md); пусто = все | — |
Classes | query (csv) | ProjectClass[]? | Нет | Standard/Comfort/Business/Premium/Elite | — |
RegionIds | query (csv) | long[]? | Нет | ≤ 20 элементов, каждый > 0 — ссылки справочника регионов (handbook-replica.md); членство RegionId in (…) (ix_projects_region_id); неизвестный/архивный id даёт пустое пересечение, не ошибку | — |
CityIds | query (csv) | long[]? | Нет | ≤ 20 элементов, каждый > 0 — ссылки справочника городов; членство CityId in (…) (ix_projects_city_id); неизвестный/архивный id даёт пустое пересечение, не ошибку | — |
ConstructionStatuses | query (csv) | ConstructionStatus[]? | Нет | Planned/UnderConstruction/Commissioned/Delivered | — |
Price | query (Price.Min/Price.Max) | NullableRange<decimal>? | Нет | Price.Min >= 0, Price.Max >= 0, при обоих — Price.Min <= Price.Max; пересечение с диапазоном проекта | — |
SortBy | query | PublicProjectSortBy | Нет (default CreatedAtDesc) | enum: CreatedAtDesc/CreatedAtAsc/PriceAsc/PriceDesc/PaybackYearsAsc/PaybackYearsDesc | — |
Enum PublicProjectSortBy
| Значение | Порядок | Ключ (индекс) |
|---|---|---|
CreatedAtDesc (default) | по дате добавления, новые первыми | CreatedAt desc (ix_projects_created_at) |
CreatedAtAsc | по дате добавления, старые первыми | CreatedAt asc |
PriceAsc / PriceDesc | по цене | Price.Min nulls last (ix_projects_price_min) |
PaybackYearsAsc / PaybackYearsDesc | по окупаемости | PaybackYears nulls last |
Модель ответа SearchPublicProjectsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Projects | Страница PublicProjectFeedCard | Да | страница выборки | см. карточку | Страница карточек ленты (Items, Page, PageSize, TotalCount, TotalPages). |
IsLimited | bool | Да | вычисляется: !IsAuthenticated | — | true — отдана ограниченная лента (цены скрыты); фронт показывает CTA «войдите». |
Карточка PublicProjectFeedCard (проекция ProjectEntity)
Masking-колонка — правило для анонима (IsAuthenticated = false); авторизованный получает значение всегда.
| Поле | Тип | Обязательность | Источник | Masking (аноним) | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | Project.Id | — | Идентификатор проекта. |
Name | string | Да | Project.Name | — | Название ЖК/отеля. |
Type | ProjectType (string) | Да | Project.Type | — | Тип (reference-data.md). |
Class | ProjectClass (string) | Да | Project.Class | — | Класс. |
ConstructionStatus | ConstructionStatus (string) | Да | Project.ConstructionStatus | — | Статус строительства. |
RegionId | long | Да | Project.RegionId | — | Ссылка на регион справочника (у Published-проекта резолвлена, INV-PRJ-3). |
RegionName | string | Да | Project.RegionName (денормализация) | — | Display-имя региона. |
CityId | long | Да | Project.CityId | — | Ссылка на город справочника (INV-PRJ-3). |
CityName | string | Да | Project.CityName (денормализация) | — | Display-имя города. |
Stars | short? | Нет | Project.Stars | — | Звёздность отеля (Type in (ApartHotel, Hotel)). |
CoverImageFileId | Guid? | Нет | Project.CoverImageFileId | — | Обложка карточки (пресайн — storage/CDN). |
Price | NullableRange<decimal>? | Нет | Project.Price (денормализация, Price.Min/Price.Max) | null (точные цены скрыты) | Диапазон цен свободных объектов недвижимости. |
CurrencyId | short | Да | Project.CurrencyId | — | Ссылка на валюту справочника (RUB-only, ADR-0048); display-код/знак — публичный lookup handbook. |
PropertyObjectsTotalCount | int | Да | Project.PropertyObjectsTotalCount | — | Всего объектов недвижимости (карточка «кол-во объектов»). |
PaybackYears | decimal? | Нет | Project.PaybackYears | — | Окупаемость, лет (детальный расчёт — invest-analytics). |
Developer | DeveloperFeedSummary | Да | join DeveloperEntity | — | Застройщик-владелец (ниже). |
PropertyObjectsFreeCount в ленту не проецируется (masked-показатель свободных объектов недвижимости живёт в карточке проекта —
get-project.md); в этой ленте маскируются только цены.
Вложенная модель DeveloperFeedSummary (проекция developer.md)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | Developer.Id | — | = CompanyId identity. |
Name | string | Да | Developer.Name | — | Название застройщика. |
LogoFileId | Guid? | Нет | Developer.LogoFileId | — | Лого (пресайн — storage/CDN). |
Предусловия и постусловия
Предусловия:
- Вызов анонимом (ограниченная лента) или авторизованным пользователем (полная); признак авторизации
определяет masking. Agents-зеркало требует авторизацию агента (
IsAuthenticated = true, masking отключён). - Публичный срез: проекты и застройщики
Published(INV-DEV-6); тот же срез применяется к полнотекстовому поиску (published-фильтр ES-проекции, ADR-0014/0059).
Постусловия (read — состояние не меняется):
- Возвращена страница карточек и
IsLimited; для анонимаPrice = nullв каждой карточке; фильтрация и сортировка по цене остаются серверными даже при маскировании выхода (сервер знает точные цены, наружу их не отдаёт). - Пустая выдача — валидный
200cItems = []. - Freshness: денормализация
Price/PropertyObjectsTotalCountотстаёт до debounce-джобаproject-aggregates-recalc(30 с); лаг ES-проекцииProjectSearchDocument— < 1 мин отproject-upserted(search/projects.md «Freshness»), поэтому только что опубликованный/снятый проект может кратковременно расходиться между структурной выборкой и полнотекстовым набором.
Алгоритм
Постраничная лента карточек проектов по published-срезу: опциональный полнотекстовый поиск сужает набор id, затем структурная выборка с фильтрами и сортировкой; для анонима цены маскируются.
- Контекст и доступ. Вызывает аноним (ограниченная лента) или авторизованный пользователь (полная);
признак авторизации определяет masking. Agents-зеркало требует авторизацию агента (
IsAuthenticated = true, masking отключён). Public-лента без гейта. Чтение — по published-срезу (проекты и застройщикиPublished). - Проверка входа.
Page >= 1,PageSizeв1..100, длинаSearch≤ 256, границы ценового диапазона>= 0иPrice.Min <= Price.Max,SortByиз перечня → иначеValidationError. Семантически несовместимый фильтр (неизвестное enum-значениеTypes/Classes/ConstructionStatusesпосле разбора csv,Price.Min > Price.Max,SortByвне перечня) →CHESSBOARD_PROJECTS_FILTER_INVALID. Id-входов нет — существование сущностей не проверяется; допустимость состояния задаёт жёсткий предикатStatus = Published. - Что читаем.
- Если задан
Search— полнотекстовый матч по Elasticsearch-проекцииProjectSearchDocument(../../../search/projects.md, ADR-0059): multi-match поName/NameTranslit/Address/RegionName(fuzziness, транслитерация) с published-фильтром (ADR-0014) → relevance-упорядоченный наборProjectId. Прямой клиент Elasticsearch в обработчике запрещён (ADR-0059). - Карточки читаются одной постраничной проекцией с join на застройщика (без загрузки агрегатов):
- публичный срез: проект
Publishedи не удалён; застройщикPublishedи не удалён (INV-DEV-6 HiddenCascades — скрытый застройщик убирает все свои проекты); - если задан
Search— только проекты из набора id выше; Types/Classes/ConstructionStatuses— членство в наборе (индексыix_projects_type/_class/_construction_status);RegionIds/CityIds— членство в наборе ссылок справочника (ix_projects_region_id/ix_projects_city_id);- ценовой диапазон — пересечение отрезков
Price.Max >= @Min && Price.Min <= @Max(ix_projects_price_min); - сортировка — по
SortBy(таблица enum выше). Тем же запросом считаетсяTotalCount.
- публичный срез: проект
- Если задан
- Сборка ответа и masking. Карточка — по таблице
PublicProjectFeedCard(плоские значения проекта,Price— диапазон, застройщик →DeveloperFeedSummary). При анониме —Price = nullв каждой карточке,IsLimited = true; для авторизованного значения отдаются как есть,IsLimited = false. Фильтрация и сортировка по цене остаются серверными даже при маскировании выхода (сервер знает точные цены, наружу не отдаёт). - Результат. Страница карточек и
IsLimited, статус200; пустая выдача —200cItems = []. Freshness: read-committed снимок; денормализацияPrice/PropertyObjectsTotalCountотстаёт до debounce-джобаproject-aggregates-recalc(30 с); лаг Elasticsearch-проекции — < 1 мин отproject-upserted(search/projects.md «Freshness»).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none |
| External calls | Elasticsearch (только при заданном Search) — чтение проекции ProjectSearchDocument (ADR-0059); пресайн обложек/лого — клиент через storage/CDN по FileId |
| Cache / projections / search | полнотекстовый Search — Elasticsearch-проекция ProjectSearchDocument (../../../search/projects.md, ADR-0059); фасеты/сортировка/пагинация — PostgreSQL read-model (btree ix_projects_*); кэша нет (ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение формата/min/max/шаблона/enum входных полей | Список ошибок полей | no retry |
CHESSBOARD_PROJECTS_FILTER_INVALID | 400 | Семантически несовместимый фильтр (Price.Min > Price.Max, неизвестный enum после парсинга csv, SortBy вне enum) | «Проверьте параметры фильтра» | retry после исправления |
INTERNAL_ERROR | 500 | Инфраструктурный сбой (PostgreSQL или Elasticsearch недоступны) | generic | retry (client backoff) |
Пустая выдача — 200 c Projects.Items = [] (не ошибка).
Совместимость и наблюдаемость
- Новые optional фильтры/поля карточки — additive (без breaking); удаление/переименование/смена типа поля или ослабление masking в сторону раскрытия цен гостю — breaking (новая версия route + rollout).
- Enum-поля сериализуются строками; клиент обязан иметь fallback на unknown-значения (reference-data.md).
- Logs: actor type (
anonymous/user), фильтры (значениеSearch— первые 64 симв.),Page/PageSize; без PII. Metrics:chessboard_public_projects_search_total{authenticated}, histogram latency (SLO p99 < 300 мс — README «Runtime И Эксплуатация»), доля маскированных ответов. Traces: spanchessboard.search-public-projectsсо сквозной корреляцией. Dashboard/runbook: chessboard-overview.
Acceptance Criteria
- AC1 (лента по умолчанию для анонима). Дано: аноним, есть published-проекты. Когда:
GETбез фильтров. Тогда:200, порядокCreatedAtDesc,Price = nullв карточках,IsLimited = true. - AC2 (полная лента для авторизованного). Дано: авторизованный пользователь. Когда:
GET. Тогда:Priceзаполнен,IsLimited = false. - AC3 (полнотекстовый поиск). Дано: задан
Search. Когда:GET. Тогда: набор id сужен ES-матчем поName/NameTranslit/Address/RegionName(published-срез), карточки читаются из PostgreSQL по этим id в порядке релевантности. - AC4 (фильтры + сортировка по цене). Дано:
Types/Classes/PriceиSortBy = PriceAsc. Когда:GETанонимом. Тогда: лента отфильтрована и упорядочена по цене серверно, ноPriceв выдаче =null. - AC5 (несовместимый фильтр). Дано:
Price.Min > Price.Maxили неизвестный enum после разбора csv. Когда:GET. Тогда:CHESSBOARD_PROJECTS_FILTER_INVALID(400). - AC6 (границы пагинации). Дано:
PageSizeвне1..100илиPage < 1. Когда:GET. Тогда:ValidationError(400). - AC7 (пустая выдача). Дано: фильтр без совпадений. Когда:
GET. Тогда:200cProjects.Items = [](не ошибка). - AC8 (каскад скрытия застройщика). Дано: застройщик не
Published. Когда:GET. Тогда: его проекты в ленте отсутствуют (INV-DEV-6). - AC9 (фильтр по ссылкам справочника). Дано:
RegionIds/CityIdsс существующими id. Когда:GET. Тогда: в выдаче только проекты с этимиRegionId/CityId; неизвестный или архивный id даёт пустое пересечение (200, не ошибка).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CHS-004. Просмотр шахматки проекта: гость vs инвестор (разница полей)
- Поиск (1): Search Document: ProjectSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: project.md, developer.md,
reference-data.md; справочные ссылки
RegionId/CityId/CurrencyId— handbook-replica.md. - Поисковая проекция: ../../../search/projects.md (
ProjectSearchDocument, ADR-0059). - Соседние операции: get-project.md (карточка), get-building.md, get-property-object.md; developers-аналог — developers-search-projects.md.
- Витрина/ранжирование ленты —
catalog(../../../../catalog/api/public/get-projects.md); трекинг просмотров/поиска —user-activity(../../../../user-activity/api/public/track-action.md, будущий путь по конвенции). - Реестр операций: ../../README.md; обзор сервиса: ../../../README.md.