GET /api/v1/public/property-objects — Список объектов недвижимости проекта/здания с фильтрами
Назначение
Табличный список объектов проекта или конкретного здания с фильтрами по этажу, комнатности, цене, площади,
планировке, видам из окон и статусу — постраничная выдача с сортировкой. Обслуживает вывод полного списка
объектов проекта с ценами и статусами, а также применение фильтров внутри проекта.
Анониму список доступен в ограниченном виде: цены, цена/м², статусы и таймеры скрыты, а фильтры/сортировки
по скрытым полям отклоняются (нельзя восстановить цену бинарным поиском по фильтру). Зеркалируется в agents
area (GET /api/v1/agents/property-objects) той же операцией; комиссия агента в списке — обогащение витрины
catalog/rewards, не этот контекст.
| Поле | Значение |
|---|
| Сервис/контекст | chessboard |
| API area | public (зеркало: agents) |
| Feature group | propertyObjects |
| Статус | draft |
| Owner | PIN Platform Core |
| Документ | docs/06-services/chessboard/api/public/property-objects/list-property-objects.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Anonymous (ограниченная проекция, урезанные фильтры) / User — инвестор, агент, менеджер застройщика (полная) |
| Auth policy / Permissions | none — public endpoint; masking и допустимость фильтров по признаку авторизации |
| Scope (RBAC) | не применяется |
| Record-level | не применяется |
| Tenant isolation | Bypass tenant-фильтра по публичному срезу: PropertyObject.Status != Hidden, здание/проект/застройщик Published |
| Audit | none (read) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/public/property-objects |
| Success status | 200 |
| Idempotency header | не применяется (GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ProjectId | query | Guid | Да | Required, != Guid.Empty; published-проект (шаг 3) | — |
BuildingId | query | Guid? | Нет | published-здание проекта (шаг 3); null — все здания проекта | — |
Floor | query (floorMin/floorMax) | NullableRange<short> | Нет | границы -5..200; From <= To при обоих заданных | — |
Rooms | query (repeat: rooms=1&rooms=2) | IReadOnlyList<short>? | Нет | каждый 0..20 (0 = студия); ≤ 10 элементов, без дублей | — |
Price | query (priceMin/priceMax) | NullableRange<decimal> | Нет | границы >= 0; From <= To при обоих заданных | только авторизованным (иначе 403 — фильтр по скрытому полю) |
AreaTotal | query (areaTotalMin/areaTotalMax) | NullableRange<decimal> | Нет | границы > 0; From <= To при обоих заданных | — |
Statuses | query (repeat) | IReadOnlyList<PropertyObjectStatus>? | Нет | значения из Free/Reserved/Booked/Sold (Hidden отклоняется всегда — 400); ≤ 4, без дублей | только авторизованным (иначе 403) |
LayoutId | query | Guid? | Нет | планировка этого проекта (шаг 3) | — |
LayoutGroupId | query | Guid? | Нет | группа планировок этого проекта; взаимоисключим с LayoutId (оба заданы → 400) | — |
WindowViews | query (repeat) | IReadOnlyList<WindowViewKind>? | Нет | значения enum (reference-data.md); ≤ 7, без дублей; семантика — «хотя бы один из» | — |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 20) | 1..100 (лента 20–30) | — |
OrderBy | query | PropertyObjectOrderField (string) | Нет (default см. ниже) | enum ниже | Price/PricePerMeter — только авторизованным (иначе 403) |
OrderDirection | query | OrderDirection (string) | Нет (default Asc) | Asc / Desc | — |
IsAuthenticated | identity claims | bool | Да (вычисляется) | из контекста авторизации | определяет masking и допустимость фильтров |
Enum PropertyObjectOrderField (поля сортировки)
| Значение | Колонка | Default | Доступно анониму |
|---|
Price | property_objects.price (index ix_property_objects_price) | default для авторизованных (Asc) | Нет (403) |
PricePerMeter | property_objects.price_per_meter | — | Нет (403) |
TotalArea | property_objects.total_area | — | Да |
Floor | property_objects.floor | default для анонима (Asc) | Да |
Rooms | property_objects.rooms (index ix_property_objects_rooms) | — | Да |
Number | property_objects.number | — | Да |
CreatedAt | property_objects.created_at (index ix_property_objects_created_at) | — | Да |
Тай-брейк любой сортировки — Number asc, Id asc (стабильная пагинация).
Модель ответа — страница PropertyObjectListItemModel
Постраничный контейнер: Items, Page, PageSize, TotalCount. Masking-колонка — правило для анонима.
| Поле | Тип | Обязательность | Источник | Masking (аноним) | Описание |
|---|
Items | IReadOnlyList<PropertyObjectListItemModel> | Да (может быть пустым) | страница выборки | построчно (ниже) | Элементы страницы. |
Page | int | Да | вход | — | Текущая страница. |
PageSize | int | Да | вход | — | Размер страницы. |
TotalCount | long | Да | число объектов под фильтром | — | Всего объектов недвижимости под фильтром. |
Вложенная модель PropertyObjectListItemModel (проекция property-object.md)
| Поле | Тип | Обязательность | Источник | Masking (аноним) | Описание |
|---|
Id | Guid | Да | PropertyObject.Id | — | Переход в карточку (get-property-object.md). |
Number | string | Да | PropertyObject.Number | — | Номер объекта. |
Floor | short | Да | PropertyObject.Floor | — | Этаж. |
Section | short? | Нет | PropertyObject.Section | — | Секция. |
Rooms | short | Да | PropertyObject.Rooms | — | Комнатность. |
TotalArea | decimal | Да | PropertyObject.TotalArea | — | Общая площадь. |
LivingArea | decimal? | Нет | PropertyObject.LivingArea | — | Жилая площадь. |
Price | decimal? | Нет | PropertyObject.Price | null | Цена. |
CurrencyId | short | Да | PropertyObject.CurrencyId | — | Ссылка на валюту справочника (RUB-only, ADR-0048); display-код/знак — публичный lookup handbook. |
PricePerMeter | decimal? | Нет | PropertyObject.PricePerMeter | null | Цена за м². |
Status | PropertyObjectStatus (string)? | Нет | PropertyObject.Status | null | Статус продажи. |
ReservedUntil | DateTimeOffset? | Нет | PropertyObject.ReservedUntil | null | Таймер резерва. |
BookedUntil | DateTimeOffset? | Нет | PropertyObject.BookedUntil | null | Таймер брони. |
WindowViews | IReadOnlyList<WindowViewKind> (string) | Да (может быть пустым) | PropertyObject.WindowViews.Views | — | Виды из окон. |
Capacity | short? | Нет | PropertyObject.Capacity | — | Вместимость (отели). |
Layout | PropertyObjectLayoutModel? | Нет | батч-загрузка LayoutEntity | — | Планировка объекта (id + название; ниже). null — без планировки. |
Building | BuildingRefModel | Да | батч-загрузка BuildingEntity | — | Здание (id, название, срок сдачи; ниже). |
ProjectId | Guid | Да | PropertyObject.ProjectId | — | Проект (переход в карточку проекта). |
Вложенная модель PropertyObjectLayoutModel (планировка объекта, батч LayoutEntity)
| Поле | Тип | Обязательность | Источник | Описание |
|---|
LayoutId | Guid | Да | Layout.Id | Идентификатор планировки. |
LayoutName | string | Да | Layout.Name | Название планировки («1К-А 32.5»); батч-загрузка, без N+1. |
Вложенная модель BuildingRefModel (ссылка на здание, батч BuildingEntity)
| Поле | Тип | Обязательность | Источник | Описание |
|---|
BuildingId | Guid | Да | Building.Id | Идентификатор здания. |
BuildingName | string | Да | Building.Name | «Литер 3»; батч-загрузка, без N+1. |
DeliveryYear | short? | Нет | Building.DeliveryYear | Срок сдачи (колонка списка). |
DeliveryQuarter | short? | Нет | Building.DeliveryQuarter | Квартал сдачи. |
Предусловия и постусловия
Предусловия:
- Вызов анонимом (ограниченная проекция, урезанные фильтры) или авторизованным пользователем (полная);
признак авторизации определяет masking и допустимость фильтров.
- Публичный срез:
PropertyObject.Status != Hidden, здание/проект/застройщик Published (INV-DEV-6).
ProjectId задан; LayoutId и LayoutGroupId не заданы одновременно.
Постусловия (read — состояние не меняется):
- Возвращена страница объектов с точным
TotalCount под фильтром на момент запроса. Для анонима в каждом
элементе Price/PricePerMeter/Status/ReservedUntil/BookedUntil = null; фильтры/сортировки по
скрытым полям отклонены (403), чтобы цену/статус нельзя было восстановить перебором.
- Пустая выдача (в т.ч. по несуществующему
LayoutId/пустой группе) — валидный 200 c Items = [].
- Freshness: прямое чтение source of truth; строки обогащаются зданиями и планировками страницы батч-загрузкой
(без N+1).
Алгоритм
Постраничный список объектов проекта/здания по published-срезу: фильтры и сортировка, с masking цены/статуса
и запретом фильтров по скрытым полям для анонима.
- Контекст и доступ. Вызывает аноним (ограниченная проекция, урезанные фильтры) или авторизованный
пользователь (полная); признак авторизации определяет masking и допустимость фильтров. Чтение — по
публичному срезу.
- Проверка входа.
- Синтаксис/диапазоны:
ProjectId задан; границы Floor/Price/AreaTotal согласованы (From <= To);
массивы без дублей и в лимитах; Statuses не содержит Hidden; LayoutId и LayoutGroupId не заданы
одновременно; Page/PageSize в границах. Нарушение → VALIDATION_ERROR.
- Права на фильтры: при анониме и заданном
Price/Statuses либо сортировке по Price/PricePerMeter →
CHESSBOARD_FILTER_REQUIRES_AUTH (фильтр/сортировка по скрытым полям позволили бы восстановить
цену/статус).
- Существование и публичность — шаг 3.
- Что читаем.
- Проект по id со статусом
Published и не удалённый; не найден → CHESSBOARD_PROJECT_NOT_FOUND.
- Застройщик проекта; статус не
Published (INV-DEV-6) → CHESSBOARD_PROJECT_NOT_FOUND.
- Область зданий: заданный
BuildingId — published-здание этого проекта (иначе →
CHESSBOARD_BUILDING_NOT_FOUND); иначе — все published-здания проекта (объекты hidden-зданий в выдачу не
попадают).
- Если задан
LayoutGroupId — планировки этой группы в проекте; пустой список → пустая выдача (валидное
«ничего не найдено»). Заданный LayoutId отдельно не проверяется (несуществующий id даёт пустую выдачу,
без утечки).
- Основная выборка объектов проекта в области published-зданий (не удалённых) с применением фильтров: этаж,
комнатность, цена, площадь, планировка/группа планировок, виды из окон («хотя бы один из»), статусы
(только для авторизованного; иначе
Status != Hidden). Сортировка — по заданному полю и направлению,
default Price asc (авторизованный) / Floor asc (аноним), тай-брейк Number asc, Id asc; страница с
TotalCount (индексы ix_property_objects_project_status, ix_property_objects_building_floor,
ix_property_objects_rooms, ix_property_objects_price).
- Обогащение страницы (без N+1): здания и планировки объектов страницы одной батч-загрузкой каждая.
- Сборка ответа и masking. Строка — по таблице
PropertyObjectListItemModel; Layout — id и название
планировки объекта (при наличии), Building — id, название, срок сдачи. При анониме в каждом элементе:
Price = PricePerMeter = null, Status = null, ReservedUntil = BookedUntil = null.
- Результат. Страница объектов, статус
200; ошибки — 400/403/404. Применённые фильтры как
действия пользователя трекает клиент → user-activity.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_ERROR | 400 | Нарушение правил таблицы входных данных (диапазоны, дубли, Hidden в Statuses, LayoutId+LayoutGroupId вместе) | «Некорректные параметры запроса» | no retry |
CHESSBOARD_FILTER_REQUIRES_AUTH | 403 | Аноним использует Price (диапазон)/Statuses или OrderBy in (Price, PricePerMeter) | «Фильтр доступен после входа» | no retry (войти) |
CHESSBOARD_PROJECT_NOT_FOUND | 404 | Проект не публичен/не существует/застройщик скрыт | «Проект не найден» | no retry |
CHESSBOARD_BUILDING_NOT_FOUND | 404 | BuildingId не является published-зданием проекта | «Здание не найдено» | no retry |
INTERNAL_ERROR | 500 | Инфраструктурный сбой | generic | retry (backoff) |
Совместимость и наблюдаемость
- Новые фильтры/поля ответа — additive; ужесточение валидации или изменение default-сортировки —
breaking (фиксируется в changelog контекста).
- Enum-строки — клиентский fallback на unknown обязателен.
- Logs:
projectId, buildingId, применённые фильтры (имена, не значения цен), actor type,
TotalCount; metrics: chessboard_public_property_objects_list_requests_total{authenticated}, latency
histogram (SLO p99 < 300 мс), chessboard_public_property_objects_list_page_size; traces: OTel span
chessboard.list-public-property-objects.
- Dashboard/runbook — chessboard-overview (README).
Acceptance Criteria
- AC1 (полный список для авторизованного). Дано: авторизованный, published-проект. Когда:
GET с
фильтрами по цене/статусу. Тогда: 200, отфильтрованная страница с ценами/статусами, default-сортировка
Price asc, тай-брейк Number asc, Id asc.
- AC2 (masking + default-сортировка анонима). Дано: аноним. Когда:
GET без фильтров по цене/статусу.
Тогда: 200, Price/PricePerMeter/Status/таймеры = null, default-сортировка Floor asc.
- AC3 (запрет фильтров по скрытым полям). Дано: аноним. Когда: задан
Price/Statuses или
OrderBy in (Price, PricePerMeter). Тогда: CHESSBOARD_FILTER_REQUIRES_AUTH (403).
- AC4 (
Hidden в фильтре статусов). Дано: любой актор. Когда: Statuses содержит Hidden. Тогда:
VALIDATION_ERROR (400) — всегда.
- AC5 (взаимоисключающие фильтры планировки). Дано: заданы и
LayoutId, и LayoutGroupId. Когда:
GET. Тогда: VALIDATION_ERROR (400).
- AC6 (здание вне проекта). Дано:
BuildingId не является published-зданием проекта. Когда: GET.
Тогда: CHESSBOARD_BUILDING_NOT_FOUND (404).
- AC7 (непубличный родитель). Дано: проект не
Published или застройщик скрыт. Когда: GET. Тогда:
CHESSBOARD_PROJECT_NOT_FOUND (404).
- AC8 (виды из окон «хотя бы один из»). Дано:
WindowViews = [Park, Sea]. Когда: GET. Тогда: в
выдаче объекты, у которых присутствует хотя бы один из указанных видов.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы