UC-CTL-001. Лента проектов (главная страница, бесконечная лента 20–30 карточек)
Назначение
Гость или авторизованный пользователь открывает главную страницу / ленту проектов (ЖК, апарт-отели)
и видит бесконечную ленту карточек с ключевыми параметрами: название, адрес, девелопер, диапазон цен,
количество объектов, окупаемость. Лента читается из витрины каталога (CatalogProjectCard) и по
умолчанию упорядочена как «Рекомендованные». Авторизованные видят точные цены и статусы, гость по
реферальной ссылке — маскированные значения.
| Поле | Значение |
|---|
| ID | UC-CTL-001 |
| Контекст | catalog (см. docs/06-services/catalog/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|
| Роль | Гость (с реф-cookie) / Инвестор / Агент / Девелопер / МенеджерПИН |
| Permissions / scope | чтение ленты — permissions не требуются; гейт: JWT или валидная реферальная cookie referralUrl; авторизованные видят точные цены/статусы, гость — маскированные |
| Tenant | не применяется — витрина platform-level (ADR-0052 п.3); TenantId актора не участвует в выборке |
| Record-level | не применяется — публичные данные; отбор только по Status = Published |
Предусловие
- Витрина
CatalogProjectCard наполнена (есть хотя бы один проект в Status = Published); источник
структуры — события pin.chessboard.projects.v1.
- У актора есть JWT (
pin:session) ИЛИ реферальная cookie referralUrl (срок 30 дней).
Триггер
| Поле | Значение |
|---|
| Источник | UI navigation |
| Триггерящий элемент | Открытие страницы /lots (лента объектов недвижимости — UC-CTL-002..004 тем же контрактом), /hotels (лента проектов-отелей) или главной страницы |
Основной Поток
- Пользователь открывает главную страницу /
/hotels.
- Интерфейс проверяет гейт: наличие JWT в
pin:session ИЛИ валидной реф-cookie referralUrl; при
отсутствии обоих — альтернатива A1 (auth-модалка).
- Интерфейс запрашивает ленту проектов с параметрами: тип проекта (
kind, enum ProjectKind,
опционально), курсор keyset-пагинации (опционально), размер страницы (1..30, по умолчанию 20),
поисковый запрос и фильтры/сортировка (см. UC-CTL-002..004).
- Каталог проверяет доступ и формирует выдачу:
- Проверка входа.
limit в диапазоне 1..30; cursor — корректный keyset-токен (кодирует
PromotionBoost|DeveloperPriority|PopularityScore|PublishedAt|Id последней карточки); kind —
известное значение enum. При нарушении — ValidationError (400).
- Гейт. Актор авторизован ИЛИ реф-cookie валидна; иначе —
CATALOG_FEED_GATE_REQUIRED (401).
- Выборка. Карточки из витрины по условию
Status = Published (+ Kind = kind, если задан),
порядок по умолчанию — «Рекомендованные»: PromotionBoost DESC → DeveloperPriority DESC → PopularityScore DESC → PublishedAt DESC → Id DESC (формула — ranking-rule.md).
- Состав карточки. Только витринные поля:
Id, название, адрес, девелопер (+лого),
диапазон цен priceFrom/priceTo, количество доступных объектов, окупаемость payback {Value, Period},
звёзды, срок сдачи, готовность, рейтинг, обложка, короткое описание (для всплывающей подсказки).
- Гостевое маскирование. Для гостя по реф-cookie без JWT поля
priceFrom/priceTo/propertyObjectsAvailable = null, masked = true.
- Результат. Список карточек,
nextCursor (null, когда страница неполная) и признак hasMore.
- Интерфейс рендерит сетку 20–30 карточек и кеширует страницу.
- Пользователь скроллит ленту; при достижении конца интерфейс повторяет шаг 3 с
cursor = nextCursor
— бесконечный скролл, эквивалент «Загрузить ещё».
- Наведение курсора на карточку показывает подсказку из короткого описания без запроса к API.
- Клик по карточке — переход на страницу проекта: UC-CTL-005 (гость) / UC-CTL-006 (авторизованный).
Альтернативные Потоки
A1. Нет ни JWT, ни реф-cookie
- Гейт не пройден (
CATALOG_FEED_GATE_REQUIRED): лента блокируется, открывается auth-модалка
(UC-IDN-001, СМС-вход).
- После входа возвращается к шагу 3 основного потока; гость, пришедший по реферальной ссылке,
получает cookie и проходит гейт без входа (маскированные данные).
A2. Пустая лента
- Шаг 4 возвращает 0 карточек (нет
Published-проектов выбранного kind).
- Ответ
{ items: [], nextCursor: null, hasMore: false }; интерфейс показывает empty-state
«Проекты скоро появятся». Сценарий завершается.
A3. Устаревший/повреждённый cursor
- Шаг 4: cursor не декодируется →
ValidationError (400).
- Интерфейс сбрасывает пагинацию и повторяет запрос без cursor (лента с начала). Возврат к шагу 3.
A4. Сессия истекла во время бесконечного скролла
- На подгрузке очередной страницы (шаг 6) JWT протух, а реф-cookie отсутствует → каталог отвечает
CATALOG_FEED_GATE_REQUIRED (401).
- Интерфейс сохраняет позицию скролла, открывает auth-модалку (как A1); после входа повторяет запрос той
же страницы с прежним
cursor — уже загруженные карточки не перезапрашиваются, лента продолжается без
потери позиции. Возврат к шагу 6.
Постусловие
- Пользователь видит актуальную (eventual, лаг < 1 мин по ценам/статусам — README.md,
раздел «Runtime») ленту
Published-проектов в порядке «Рекомендованные».
- Только что опубликованный в источнике проект попадает в ленту после доезда консюмера
chessboard и
пересчёта витрины (окно секунды–минуты); до этого он в выдаче отсутствует — это ожидаемое промежуточное
состояние, не ошибка. Симметрично, скрытый/делистнутый проект исчезает из ленты в том же окне.
- Состояние системы не изменилось: сущности не создавались и не мутировались; события не публиковались.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
ValidationError | 400 | limit вне 1..30, неизвестный kind, битый cursor | Сброс параметров, повтор без cursor |
CATALOG_FEED_GATE_REQUIRED | 401 | Нет JWT и нет валидной реф-cookie | Открыть auth-модалку (A1) |
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | В ленту попадают только карточки Status = Published; Hidden/Delisted исключены | интеграционный тест: скрыть проект оверрайдом → карточка исчезает из ответа |
| AC2 | Порядок: PromotionBoost DESC → DeveloperPriority DESC → PopularityScore DESC → PublishedAt DESC | тест с карточками разных boost/priority |
| AC3 | Keyset-пагинация: страницы не пересекаются и не теряют карточки при вставке новых | тест: 3 страницы по 20 при конкурентной публикации |
| AC4 | Гость (реф-cookie без JWT): priceFrom/priceTo/propertyObjectsAvailable = null, masked = true; авторизованный видит точные значения | два запроса с разными акторами |
| AC5 | Без JWT и без реф-cookie — 401 CATALOG_FEED_GATE_REQUIRED | запрос без credentials |
| AC6 | Ответ ленты собирается из одной витрины catalog.project_cards без джойнов в источники | проверка выборки |
| AC7 | Истечение JWT на подгрузке страницы → 401 CATALOG_FEED_GATE_REQUIRED; после входа лента продолжается с того же cursor без потери/дублей карточек | тест протухания токена в середине пагинации |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы
- API метод:
../api/public/get-projects.md (контракт ленты/поиска/фильтров/сортировки в одном
endpoint-е).
- Domain entity: ../domain/catalog-project-card.md (дом полей,
ленточный индекс), ../domain/ranking-rule.md (формула «Рекомендованные»),
../consumers/promotion-boost.md (
PromotionBoost — консюмер promotion).
- Frontend: страницы
/hotels, /lots (гейт, бесконечная лента).
- Связанные UC: UC-CTL-002 (поиск), UC-CTL-003 (фильтры), UC-CTL-004 (сортировка),
UC-CTL-005/006 (карточка проекта); вход по реферальной ссылке —
docs/06-services/referral/.