UC-CTL-004. Сортировка проектов (цена / дата добавления / популярность / окупаемость)
Назначение
Пользователь сортирует ленту проектов/объектов недвижимости по цене, дате добавления, популярности или
окупаемости. Все ключи сортировки — денормализованные поля витрины (MinPrice/Price, PublishedAt,
PopularityScore, Payback). По умолчанию действует порядок «Рекомендованные» (ранжирование застройщика
и пакеты продвижения — ranking-rule.md); явные сортировки пользователя —
честные, ранжированием не искажаются. Выбранная сортировка запоминается на сессию через user-activity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CTL-004 |
| Контекст | catalog (см. docs/06-services/catalog/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Гость (с реф-cookie) / Инвестор / Агент / Девелопер / МенеджерПИН |
| Permissions / scope | как UC-CTL-001: гейт ленты; permissions не требуются |
| Tenant | не применяется — платформенная витрина (ADR-0052 п.3) |
| Record-level | не применяется |
Предусловие
- Выполняются предусловия UC-CTL-001; лента открыта (возможно, с поиском/фильтрами).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI select |
| Триггерящий элемент | Выпадающий список сортировки ленты |
Основной Поток
-
Пользователь выбирает вариант сортировки из выпадающего списка: «по цене», «по дате добавления», «по популярности», «по окупаемости» и, при поддержке варианта, порядок возрастание/убывание.
-
Интерфейс кладёт
sortв URL-параметры (восстановление/шаринг вместе с фильтрами — как UC-CTL-003) и запрашивает ленту с сохранением текущихsearchTerm/фильтров. Значение сортировки — enumCatalogSort(по умолчаниюrecommended):Значение Ключи ORDER BY(карточка проекта)recommendedPromotionBoost DESC, DeveloperPriority DESC, PopularityScore DESC, PublishedAt DESC, Id DESCpriceAsc/priceDescMinPrice ASC NULLS LAST / DESC NULLS LAST, IddateDescPublishedAt DESC, Id DESC(«сначала новые»)popularityPopularityScore DESC, Idpaybackнормализованная окупаемость в годах ASC NULLS LAST ( payback_valueприPeriod=Years,payback_value/12приMonths— правило catalog-property-object-card.md),Id -
Каталог применяет сортировку:
- Проверка входа.
sort— известное значениеCatalogSort; направление допустимо только для цены (priceAsc/priceDesc— отдельные значения enum); иначеValidationError(400). Keyset-cursor версионируется значениемsort: cursor от другой сортировки отклоняется валидацией. - Выборка. Карточки
Status = Publishedс активными фильтрами и поиском, порядок — по ключам выбранной сортировки (таблица шага 2). - Ранжирование.
PromotionBoost/DeveloperPriorityучаствуют только вrecommended— прозрачность явных сортировок (решение — ranking-rule.md); «по популярности» используетPopularityScore, рассчитанный по завершённым просмотрам (pin.user-activity.property-object-view-ended.v1, ADR-0056 п.3, ночной jobpopularity-recompute). - Состав карточек и гостевое маскирование — как UC-CTL-001.
- Результат. Отсортированный список.
- Проверка входа.
-
Интерфейс обновляет ленту в новом порядке.
-
Пагинация продолжает выбранную сортировку (cursor несёт её ключи).
-
Интерфейс запоминает сортировку для текущей сессии: локально (sessionStorage + URL) и действием
SortChangedвuser-activity(ADR-0054) — при возврате в ленту в рамках сессии применяется сохранённое значение.
Альтернативные Потоки
A1. Сортировка ленты объектов недвижимости /lots
- Тот же сценарий для ленты объектов недвижимости
/lots; ключи уровня объекта:priceAsc/priceDesc— эффективнаяPrice,dateDesc—PublishedAt,popularity—PopularityScore,payback— нормализацияPropertyObjectPaybackв годы,recommended— те же ключи ранжирования. Завершается как основной поток.
A2. Смена сортировки при активном cursor-е
- Пользователь меняет сортировку после подгрузки нескольких страниц.
- Интерфейс сбрасывает cursor и запрашивает первую страницу новой сортировки (cursor старой сортировки каталогом отклоняется — шаг 3). Возврат к шагу 3.
A3. Карточки без значения ключа (MinPrice = null, Payback = null)
NULLS LAST: карточки без цены/окупаемости попадают в конец выдачи независимо от направления; из сортировки не исключаются (остаются доступными). Поток не меняется.
A4. Сортировка «по популярности» и ночной пересчёт
- Ключ
PopularityScoreпересчитывает ночной jobpopularity-recomputeпо завершённым просмотрам за окно 30 дней с экспоненциальным затуханием; сырой счётчикPopularityViewsрастёт в течение дня по событиямpin.user-activity.property-object-view-ended.v1, но ключ сортировки от него в пределах суток не меняется. - Поэтому порядок «по популярности» стабилен внутри суток и сдвигается после ночного пересчёта: пользователь видит согласованный порядок в рамках сессии — это ожидаемое поведение, а не рассинхронизация. Поток не меняется.
Постусловие
- Лента упорядочена по выбранному ключу; порядок стабилен (tie-break
Id) и воспроизводим по URL. - Выбранная сортировка сохранена для сессии (sessionStorage +
ActionEvent (SortChanged)вuser-activity). - Состояние каталога не изменилось (read-only).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Неизвестное значение sort; cursor не соответствует sort | Сбросить cursor, повторить первую страницу |
CATALOG_FEED_GATE_REQUIRED | 401 | Гейт ленты не пройден | Auth-модалка (UC-CTL-001 A1) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
ActionEventRecorded (производит user-activity) | pin.user-activity.actions.v1 | после приёма события смены сортировки (шаг 6) | crm (контекст лида) |
PropertyObjectViewEnded (производит user-activity) | pin.user-activity.property-object-view-ended.v1 | вход данных сортировки «по популярности» (consumer каталога инкрементирует PopularityViews) | catalog, dev-stats |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Все четыре сортировки работают: цена (двунаправленно), дата, популярность, окупаемость | тесты каждого значения CatalogSort |
| AC2 | Сортировка применяется поверх активных фильтров и поиска | комбинированный тест |
| AC3 | В явных сортировках PromotionBoost/DeveloperPriority не участвуют (честная выдача) | карточка с максимальным boost не всплывает в priceAsc |
| AC4 | payback нормализуется в годы: Value/12 при Period=Months | карточки Years=2 и Months=18 упорядочены 1.5 < 2 |
| AC5 | null-ключи — в конце выдачи (NULLS LAST), карточки не пропадают | тест с MinPrice = null |
| AC6 | Сортировка запоминается на сессию: возврат в ленту восстанавливает её; создан ActionEvent (SortChanged) | e2e + проверка записи |
| AC7 | Keyset-пагинация стабильна внутри каждой сортировки (tie-break Id) | тест дубликатов на границах страниц |
| AC8 | «По популярности» использует PopularityScore (ночной пересчёт), а не сырой PopularityViews: порядок стабилен внутри суток, дневной рост просмотров его не тасует | тест инкремента просмотров без ночного job-а |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод:
../api/public/get-projects.md,../api/public/get-property-objects.md. - Domain entity: ../domain/ranking-rule.md (формула «Рекомендованные»,
прозрачность явных сортировок), ../domain/catalog-project-card.md
/ ../domain/catalog-property-object-card.md (ключи сортировок,
PopularityScore), ../consumers/promotion-boost.md (PromotionBoost). - Job:
../background/popularity-recompute.md. - Связанные UC: UC-CTL-001..003 (лента/поиск/фильтры).