GET /api/v1/developers/projects — Поиск/список проектов застройщика
Назначение
Первый экран раздела «Проекты» кабинета застройщика: список проектов своей компании с пагинацией, поиском
по названию, фильтрами (статус / тип / класс / статус строительства / регион / город) и сортировками. Менеджер
видит все свои проекты, включая Draft/Hidden/Archived — в отличие от публичной ленты, которая отдаёт
только Published. Дом полей — сущность Проект.
Поиск по названию идёт по read-model PostgreSQL (case-insensitive substring), а не по
Elasticsearch-проекции ProjectSearchDocument: кабинет застройщика ищет и неопубликованные проекты
(Draft/Hidden/Archived), а проекция держит fail-closed только Published-срез
(../../search/projects.md). Набор проектов одного tenant-а невелик — substring
достаточно (ADR-0056).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | projects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/search-projects.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.projects.manage (../../identity/domain/permission.md) |
| Scope (RBAC) | организация; сужение «группа проектов/проект» фильтрует выдачу по разрешённым ProjectId |
| Tenant isolation | TenantId = CompanyId (= DeveloperId) из JWT; выборка ограничивается tenant-ом актора (tenant_id = TenantId) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/developers/projects |
| Success status | 200 |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
Page | query | int | Нет (default 1) | >= 1 | — |
PageSize | query | int | Нет (default 20) | 1..100 | — |
Search | query | string? | Нет | <= 256 симв.; case-insensitive substring по Name + Location.AddressLine (свои проекты, включая неопубликованные) | untrusted; параметризованный LIKE |
Statuses | query (csv) | ProjectStatus[]? | Нет | значения enum Draft/Published/Hidden/Archived; пусто = все | — |
Types | query (csv) | ProjectType[]? | Нет | ResidentialComplex/ApartHotel/Hotel/Villa (reference-data.md) | — |
Classes | query (csv) | ProjectClass[]? | Нет | Standard/Comfort/Business/Premium/Elite | — |
ConstructionStatuses | query (csv) | ConstructionStatus[]? | Нет | Planned/UnderConstruction/Commissioned/Delivered | — |
RegionId | query | long? | Нет | > 0; ссылка на справочник регионов handbook — равенство по Project.RegionId | — |
CityId | query | long? | Нет | > 0; ссылка на справочник городов handbook — равенство по Project.CityId | — |
SortBy | query | ProjectSortBy | Нет (default CreatedAtDesc) | enum: CreatedAtDesc / CreatedAtAsc / NameAsc / PriceAsc / PriceDesc | — |
IncludeArchived | query | bool | Нет (default false) | false → фильтр DeletedAt == null | — |
Модель ответа SearchDeveloperProjectsResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Items | DeveloperProjectListItem[] | Да | проекция ProjectEntity | — | Страница проектов. |
Page | int | Да | echo запроса | — | Текущая страница. |
PageSize | int | Да | echo запроса | — | Размер страницы. |
TotalCount | long | Да | подсчёт под фильтром | — | Всего записей под фильтром. |
Модель DeveloperProjectListItem
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | ProjectEntity.Id | — | Id проекта. |
Name | string | Да | Name | — | Название. |
Status | ProjectStatus | Да | Status | — | Статус публикации. |
Type | ProjectType | Да | Type | — | Тип проекта. |
Class | ProjectClass | Да | Class | — | Класс. |
ConstructionStatus | ConstructionStatus | Да | ConstructionStatus | — | Статус строительства. |
RegionId | long? | Нет | RegionId | — | Регион — ссылка на справочник handbook. |
RegionName | string? | Нет | RegionName | — | Display-имя региона (денормализация; заполнено ⇔ RegionId != null). |
CityId | long? | Нет | CityId | — | Город — ссылка на справочник. |
CityName | string? | Нет | CityName | — | Display-имя города (заполнено ⇔ CityId != null). |
CoverImageFileId | Guid? | Нет | CoverImageFileId | — | Обложка (FileId storage/CDN). |
Price | NullableRange<decimal>? | Нет | Project.Price (денормализация, Price.Min/Price.Max) | — | Диапазон цен свободных объектов недвижимости. |
CurrencyId | short | Да | CurrencyId | — | Валюта — ссылка на справочник handbook (RUB-only, ADR-0048). |
PropertyObjectsTotalCount | int | Да | PropertyObjectsTotalCount | — | Всего объектов недвижимости. |
PropertyObjectsFreeCount | int | Да | PropertyObjectsFreeCount | — | Свободных объектов недвижимости. |
SyncSource | ChessboardSyncSource | Да | SyncSource | — | Pin / TalentChessboard (UI блокирует реплика-поля). |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Дата создания. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Последнее изменение. |
Version | long | Да | Version | — | Доменная версия (для последующих PUT). |
Предусловия
- Пользователь авторизован;
tenant_id= его компания-застройщик. - У актора есть permission
chessboard.projects.manageхотя бы в одном scope компании (иначеFORBIDDEN). - Выдача заведомо ограничена своим tenant-ом; чужие проекты недостижимы в принципе (не «скрыты», а вне выборки).
Алгоритм
Операция read-only, доменных событий не публикует.
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.projects.manageв scopeOrganization. Нет permission ни в одном scope →FORBIDDEN(403); просроченный/отозванный доступ →UNAUTHORIZED(401) /FORBIDDEN(403). При сужении scope «группа проектов/проект» из назначения извлекается набор разрешённыхProjectId, которым ограничивается выдача (проекты вне scope не показываются и не считаются вTotalCount— не ошибка). - Проверка входа. Диапазоны
Page/PageSize, длинаSearch, положительностьRegionId/CityId, парсинг csv-enum-ов — неизвестное значение →ValidationError(400). - Выборка. Страница проекций собирается по фильтрам: tenant-фильтр; при
IncludeArchived = false— только не удалённые;Statuses/Types/Classes/ConstructionStatuses— совпадение по множеству;RegionId/CityId— равенство по ссылке справочника (индексыix_projects_region_id/ix_projects_city_id; несуществующий id справочника даёт пустой результат, не ошибку);Search— case-insensitive substring поNameиLocation.AddressLine(набор tenant-а мал, ADR-0056; опц. trigram-индексix_projects_name_trgm); scope-сужение — по разрешённымProjectId. Сортировка — поSortBy:CreatedAtDesc/Asc(ix_projects_created_at),NameAsc(ix_projects_name),PriceAsc/Desc— поPrice.Minnulls last (ix_projects_price_min). СчитаетсяTotalCountпод тем же фильтром. Пустой результат и запрос страницы за пределами последней — валидны:200,Items = [], фактическийTotalCount. - Результат. Возвращаются
Items,Page,PageSize,TotalCount; HTTP200. Маскирование не требуется (PII нет). Freshness: read-committed снимок; денормализацииPrice,PropertyObjects*Countмогут отставать до фонового пересчёта агрегатовproject-aggregates-recalc(окно ~40 с).
Acceptance Criteria
- Happy path. Given tenant с проектами в разных статусах; When
GET .../projects; Then200со страницей своих проектов (включаяDraft/Hidden/Archived), отсортированной поCreatedAtDesc. - Фильтр + поиск. Given
Statuses=Published&Search=<часть названия>; When запрос; Then толькоPublished-проекты, чьёName/адрес содержат подстроку. - Сужение scope. Given scope сужен до одной группы проектов; When запрос без фильтров; Then только
проекты из разрешённого набора; проекты вне scope отсутствуют и не учтены в
TotalCount. - Пустой/за-предельный. Given фильтр без совпадений или
Pageза последней страницей; When запрос; Then200,Items = []. - Невалидный вход. Given
PageSizeвне1..100или неизвестныйProjectType; When запрос; Then400ValidationError. - Нет права. Given нет
chessboard.projects.manage; When запрос; Then403FORBIDDEN.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Неверные paging/enum/длины | Список ошибок полей | no retry |
FORBIDDEN | 403 | Нет chessboard.projects.manage | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional фильтры/поля ответа — additive; изменение семантики сортировок — breaking (rollout).
- Logs:
TenantId, фильтры (без значенийSearch); metrics:chessboard_developers_projects_search_total, latency p99 < 300 мс (SLO README); traces: OTel (traceparent).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId} — Карточка проекта застройщика, GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/project.md; реплика справочников handbook (гео/валюта): ../domain/handbook-replica.md; реестр: README.md.
- Поисковая проекция (публичная лента): ../../search/projects.md.
- Карточка: get-project.md.