Catalog Public API — реестр методов и общие правила
Реестр public-методов витрины каталога (контекст catalog, сервис Pin.Catalog.Api, area public)
и общие правила этой области: модель акторов и гейт ленты, правила маскирования для гостя,
keyset-пагинация ленты, сортировки, общие response-модели. Документы методов ссылаются сюда и
описывают только свою дельту. Дома полей — ../domain/.
| Поле | Значение |
|---|
| Сервис/контекст | catalog |
| API area | public |
| Статус | draft |
| Owner | PIN Marketplace |
| Область действия | лента/поиск/фильтры/сортировки/карточки/ассортимент; developers- и admin-области — отдельные реестры (api/developers/, api/admin/) |
| Assumptions | реф-cookie referralUrl ставится фронтом (referral, cookie 30 дней) |
Реестр методов
| Метод | Route | Актор (мин.) | Гейт | Назначение | Документ |
|---|
| GET | /api/v1/projects | Guest | нет (маскирование) | Лента проектов (главная, /hotels) | get-projects.md |
| GET | /api/v1/projects/{id} | Guest | нет (ограниченная) / JWT‖реф-cookie (полная) | Карточка проекта | get-project.md |
| GET | /api/v1/projects/{id}/lots | RefGuest | JWT или реф-cookie | Ассортимент проекта/отеля | get-project-property-objects.md |
| GET | /api/v1/lots | RefGuest | JWT или реф-cookie | Лента объектов недвижимости /lots | get-property-objects.md |
| GET | /api/v1/lots/{id} | Guest | нет (ограниченная) / JWT‖реф-cookie (полная) | Карточка объекта недвижимости | get-property-object.md |
| GET | /api/v1/catalog/filters | Guest | нет | Lookup справочников значений фильтров | get-catalog-filters.md |
Ошибки — ProblemDetails с машинным полем code.
Модель акторов и гейт ленты (GATE-FEED)
Класс актора определяется до выполнения бизнес-логики:
| Класс актора | Условие | Что видит |
|---|
User | валидный Bearer JWT (подпись, exp, notBefore) | полные карточки; агент с permission catalog.commissions.view — дополнительно комиссии |
RefGuest | JWT отсутствует, cookie referralUrl непустая (формат — валидный URL ≤ 2048 символов; глубокая валидация ссылки — контекст referral, каталог синхронно не вызывает — simplicity-first, ADR-0056) | полные карточки и ленты, без комиссий; инвест-калькуляторы на фронте — readOnly |
Guest | нет JWT и нет cookie | только методы без гейта, с маскированием MASK-* |
Правила:
- GATE-FEED: методы с гейтом (
/lots, /projects/{id}/lots, полные карточки) требуют User
или RefGuest; Guest → 401 (фронт открывает auth-модалку или предлагает реферальную
ссылку).
- Невалидный/просроченный JWT — всегда
401 (фронт очищает сессию); fallback на cookie
не выполняется (протухший токен ≠ гость).
- Классы актора — только правило видимости ответа; записей о госте каталог не создаёт
(сессии/
anonymous_id — контекст user-activity, ADR-0054).
Правила маскирования
| Код | Кому применяется | Правило |
|---|
MASK-PRICE | Guest (лента проектов, ограниченная карточка проекта) | Точные цены скрываются: priceRange.min округляется вниз до 100 000 ₽, priceRange.max — вверх до 100 000 ₽; pricePerMeter → null. Принятое расхождение: ограниченная карточка объекта недвижимости показывает точную цену (явно предусмотрено для ограниченной карточки объекта). |
MASK-STATUS | Guest | Статусы объектов скрываются: propertyObjectStatus → null, propertyObjectsAvailable → null (остаётся propertyObjectsTotal). |
MASK-ANALYTICS | Guest (ограниченная карточка объекта недвижимости/проекта) | Метрики доходности скрываются: payback, reliability, growthPercent, rentalIncomePerYear, rating → null; ссылки на детальную аналитику не отдаются. |
MASK-COMMISSION | все, кроме акторов с permission catalog.commissions.view | agentCommission → null (сериализация поля пропускается). Источник данных — PropertyObjectAgentCommission (../domain/catalog-property-object-card.md). Feature flag catalog.commissions.enabled=false → null для всех. |
Маскирование применяется при формировании ответа (шаг результата), не в хранении и не в запросе к БД:
одна и та же проекция обслуживает все классы акторов.
Keyset-пагинация ленты
Лента отдаёт по 20–30 карточек с бесконечным скроллом; классическая offset-пагинация не используется
(дрейф страниц при пересчёте проекций).
| Параметр | Тип | Правило |
|---|
limit | int | 1..30, default 24 |
cursor | string? | base64url(JSON FeedCursorModel); отсутствует — первая страница |
nextCursor | string? (ответ) | курсор следующей страницы; null — лента исчерпана |
FeedCursorModel (typed, ContractVersion = 1)
| Поле | Тип | Обязательность | Описание |
|---|
ContractVersion | int | Да | = 1; другое значение → CATALOG_CURSOR_INVALID. |
Sort | FeedSort | Да | Сортировка, под которую построен курсор; несовпадение с query → CATALOG_CURSOR_INVALID. |
Descending | bool | Да | Направление; несовпадение → CATALOG_CURSOR_INVALID. |
LastSearchRank | decimal? | Нет | ts_rank_cd последней карточки (только поиск + Recommended). |
LastPromotionBoost | int? | Нет | Ключ 1 Recommended. |
LastDeveloperPriority | int? | Нет | Ключ 2 Recommended. |
LastPopularityScore | decimal? | Нет | Ключ 3 Recommended / ключ Popularity. |
LastPrice | decimal? | Нет | Ключ Price (для проектов — min_price). |
LastPaybackYears | decimal? | Нет | Ключ Payback (нормализация в годы — ../domain/catalog-property-object-card.md, PropertyObjectPayback). |
LastPublishedAt | DateTimeOffset? | Нет | Ключ PublishedAt / финальный ключ Recommended. |
LastId | Guid | Да | Tie-break: id последней карточки страницы. |
Курсор не подписывается: он не содержит секретов, а подмена значений даёт лишь другую страницу
публичной выдачи. Обязательные для выбранной сортировки Last*-поля отсутствуют →
CATALOG_CURSOR_INVALID.
Пустая лента и промежуточные состояния при скролле
- Пустая выдача (фильтры сузили до нуля или в каталоге нет подходящих карточек) —
200 с пустым
массивом и nextCursor = null (не ошибка). Первая же страница с null-курсором — конец ленты.
- Дрейф между страницами: витрина пересчитывается событиями источников и мутациями оверрайдов/
ранжирования во время бесконечного скролла. Keyset (в отличие от offset) устойчив к вставке/
удалению строк, но карточка, чьи ключи сортировки изменились между страницами (напр.
PromotionBoost/
DeveloperPriority пересчитаны), может повторно попасть в следующую страницу или быть
пропущена. Это допустимо для публичной ленты; клиент дедуплицирует карточки по id при склейке
страниц. Гарантий «ровно один раз на карточку за сессию скролла» лента не даёт.
- Свежесть относительно источника: карточка отражает последнее применённое событие
chessboard/
invest-analytics/rewards/promotion (окно свежести — секунды–минуты, лаг-алерты — README сервиса,
Runtime). В промежутке пользователь может видеть устаревшие цену/статус/метрику. Это только
отображение: критичные для брони проверки статуса объекта выполняет crm в источнике на шаге
бронирования, поэтому устаревший propertyObjectStatus в ленте не приводит к переспродаже.
Сортировки (FeedSort, enum-string)
| Значение | Default направление | Ключи ORDER BY (после ключа — Id тем же направлением) | Индекс |
|---|
Recommended (default) | только desc (фиксировано) | [при searchTerm: ts_rank_cd(NameVector, query) DESC] → PromotionBoost DESC → DeveloperPriority DESC → PopularityScore DESC → PublishedAt DESC | ix_*_feed (формула — ../domain/ranking-rule.md) |
Price | asc | Price (объекты недвижимости) / MinPrice NULLS LAST (проекты) | ix_*_price |
PublishedAt | desc | PublishedAt | ix_*_published_at |
Popularity | desc | PopularityScore | ix_*_popularity |
Payback | asc | payback_years (вычисление в query) NULLS LAST | по status + вычисление |
Ранжирование застройщика и пакеты продвижения действуют только в Recommended
(принятое решение — ../domain/ranking-rule.md); явные сортировки честные.
Параметр desc: bool? инвертирует направление (кроме Recommended).
Сохранение фильтров и трекинг (кто чем владеет)
- URL — источник состояния фильтров: все фильтры/сортировка/поиск передаются query-параметрами GET;
ответы ленты возвращают echo применённых фильтров (
appliedFilters) для синхронизации URL фронтом.
Каталог состояние фильтров не хранит.
- «Фильтры сессии» и история поиска — фронт шлёт typed
ActionEvent
(SearchPerformed/FilterApplied) в user-activity
(../../user-activity/api/public/track-action.md);
каталог эти события не пишет (ADR-0054).
- Просмотры карточек — фронт шлёт события просмотра в
user-activity; популярность возвращается в
каталог консюмером pin.user-activity.property-object-view-ended.v1 (ADR-0056 п.3).
Общие response-модели (методы ссылаются)
NullableRange<decimal> (диапазон цены)
Диапазоны *Min/*Max не разносятся парой полей, а моделируются одним типом NullableRange<T>.
Ценовой диапазон витрины — NullableRange<decimal>; сериализуется как { min, max }.
| Член | Тип | Обязательность | Описание |
|---|
min | decimal? | Нет | CatalogProjectCard.MinPrice; null — нет доступных объектов недвижимости; для Guest — округление MASK-PRICE. |
max | decimal? | Нет | CatalogProjectCard.MaxPrice; аналогично. |
PaybackModel (проекция VO PropertyObjectPayback)
| Поле | Тип | Обязательность | Описание |
|---|
value | decimal | Да | PropertyObjectPayback.Value. |
period | string (enum PaybackPeriod: Years/Months) | Да | PropertyObjectPayback.Period. |
ReliabilityModel (проекция VO PropertyObjectReliability)
| Поле | Тип | Обязательность | Описание |
|---|
value | short | Да | PropertyObjectReliability.Value. |
max | short | Да | PropertyObjectReliability.Max (значение — 10). |
AgentCommissionModel (проекция VO PropertyObjectAgentCommission; MASK-COMMISSION)
| Поле | Тип | Обязательность | Описание |
|---|
percentMin | decimal | Да | PropertyObjectAgentCommission.PercentMin. |
percentMax | decimal | Да | PropertyObjectAgentCommission.PercentMax; равен percentMin, если правило одно. |
baseKind | string (enum CommissionBaseKind: BasePrice/FinalPrice) | Да | База расчёта (дом — rewards/domain/reward-rule.md). |
RewardRuleId/RuleVersion — внутренние ключи, в ответы не сериализуются.
GeoLocationModel (проекция VO ProjectGeoLocation)
| Поле | Тип | Обязательность | Описание |
|---|
latitude | decimal | Да | -90..90. |
longitude | decimal | Да | -180..180. |
DeveloperRefModel (застройщик — ref-модель)
Пара id + name не разносится плоскими полями, а сворачивается во вложенную ref-модель
(поля-источники — дом ../domain/catalog-property-object-card.md /
../domain/catalog-project-card.md).
| Поле | Тип | Обязательность | Описание |
|---|
developerId | Guid | Да | DeveloperId. |
developerName | string | Да | DeveloperName (денормализовано из chessboard). |
developerLogoFileId | Guid? | Нет | DeveloperLogoFileId (пресайн — storage/CDN); присутствует в моделях проекта. |
BuildingRefModel (корпус — ref-модель)
| Поле | Тип | Обязательность | Описание |
|---|
buildingId | Guid | Да | BuildingId. |
buildingName | string | Да | BuildingName. |
Наблюдаемость и совместимость (общие правила area)
- Logs: класс актора, применённые фильтры/сортировка,
limit, размер выдачи, флаг маскирования —
без значений searchTerm целиком (обрезка до 64 симв. — untrusted input) и без PII (PII в
витрине нет). Metrics: catalog_feed_requests_total{method,actor_class,result}, latency histogram
per route, catalog_feed_empty_results_total, catalog_commission_masked_total. Traces: span
на handler, correlation — сквозной trace через traceparent. Dashboard catalog-overview; runbook —
сверка/rebuild проекций (../README.md, Runtime).
- Совместимость: новые optional-поля моделей — additive; переименование/удаление/смена типа поля,
изменение правил MASK-* в сторону раскрытия, изменение схемы
FeedCursorModel — breaking
(ContractVersion курсора + migration note). Неизвестные значения enum в query →
ValidationError; старые курсоры после breaking-смены → CATALOG_CURSOR_INVALID (фронт
перезапрашивает первую страницу).
- Rate limiting: public-методы — общий лимит gateway (значение — конфигурация платформы), защита от
скрейпинга выдачи; каталог дополнительных лимитов не вводит (simplicity-first).
Связанные документы