Перейти к основному содержимому

UC-SEL-001. Создание подборки из текущего избранного

Назначение

Описывает сценарий создания подборки из текущего избранного владельца: сервер читает состав избранного пользователя через internal API favorites и создаёт независимую подборку (одноразовое копирование, INV-SEL-010 — дальнейшие изменения избранного подборку не меняют и наоборот). Заметки избранного копируются в комментарии элементов только по явному флагу copyNotes (заметка приватна, комментарий подборки публичен). Экран — страница /collection (подборки из избранного). Связан с контекстом selections.

Metadata

ПолеЗначение
IDUC-SEL-001
Контекстselections (см. docs/06-services/selections/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор или Агент (авторизованный пользователь). Гость подборки не создаёт — подборка привязана к владельцу-пользователю (../README.md «Известные Ограничения»)
Permissions / scopeself-scope владельца (JWT, claim sub); отдельных permissions нет (INV-SEL-002)
Tenantне tenanted (ADR-0052 п.2 — подборка личная)
Record-levelOwnerUserId = actor.UserId; internal-чтение favorites — строго по userId = actor.UserId

Предусловие

  • Пользователь авторизован (JWT).
  • В избранном пользователя (UserId-записи favorites) есть хотя бы один объект недвижимости; гостевое избранное сессии предварительно слито в аккаунт (../../favorites/use-cases/UC-FAV-002-sign-in-favorites-merge.md) — «текущее избранное» = избранное аккаунта.
  • У владельца < 100 не удалённых подборок (INV-SEL-007).

Триггер

ПолеЗначение
ИсточникUI button → API call
Триггерящий элементСтраница /favorites → кнопка «Создать подборку из избранного»; страница /collection → «Новая подборка → из избранного»

Основной Поток

  1. Пользователь нажимает «Создать подборку из избранного», вводит название (и опционально описание), выбирает флаг «Перенести мои заметки в комментарии подборки» (по умолчанию выключен — фронт предупреждает, что комментарии будут видны всем, кто откроет ссылку).
  2. Фронт отправляет POST /selections/from-favorites с телом CreateSelectionFromFavoritesForm { title: string (1..256 после trim), description?: string (≤ 2000), copyNotes: bool = false } (JWT владельца).
  3. Проверка входа и лимита. title — 1..256 после trim, description — ≤ 2000; нарушение → ValidationError (400). У владельца должно быть < 100 не удалённых подборок (INV-SEL-007) — иначе SELECTIONS_LIMIT_EXCEEDED (422, поток A5). Существование объектов недвижимости не проверяется (INV-SEL-008). Повторный вызов сознательно создаёт новую подборку (пользователь может снимать «снимки» избранного многократно); защита от двойного клика — на фронте (disable кнопки).
  4. Чтение избранного. Синхронный m2m-вызов internal API favorites (GET /internal/favorites?userId={actor.UserId}) отдаёт список { propertyObjectId, note, createdAt } (контракт — ../../favorites/README.md «Internal»). Недоступность/таймаут → SELECTIONS_FAVORITES_UNAVAILABLE (503, поток A1); пустой список → SELECTIONS_FAVORITES_EMPTY (422, поток A2). Чтение — снапшот момента вызова, вне записи подборки (расхождение на секунды допустимо — INV-SEL-010 всё равно отвязывает копию).
  5. Формирование состава. Избранное упорядочивается по createdAt desc (новые лайки — первыми). Если элементов > 200 (INV-SEL-004) — берутся первые 200, фиксируется SkippedCount = N − 200 (поток A3). Каждому элементу: PropertyObjectId ← favorite.propertyObjectId, SortOrder ← позиция 0..N−1 (INV-SEL-006), Comment ← copyNotes ? favorite.note : null (обрезка не нужна — оба лимита 4000). Дедупликация INV-SEL-003 гарантирована уникальностью пар в favorites (INV-FAV-002).
  6. Создание подборки. Создаётся подборка Source = Favorites с уникальным неизменяемым PublicCode (12 символов base62, CSPRNG — INV-SEL-005; коллизия unique-индекса ux_selections_public_code регенерируется, до 3 попыток — поток A4). Подборка и её элементы создаются в одном согласованном изменении; в том же изменении атомарно фиксируется факт создания (внутренний доменный факт SelectionCreatedEvent).
  7. Публикация факта. В той же атомарной фиксации публикуются: pin.selections.selection-created.v1 (Source = Favorites; payload: SelectionId, OwnerUserId, Title, Source, ItemsCount, OccurredAt — без списка PropertyObjectId и комментариев, ../domain/selection.md «События») и по одному pin.selections.selection-item-added.v1 на каждый скопированный объект недвижимости (M событий, ключ = SelectionId, порядок — Version; дом контракта — ../events/selection-item-added.md) — они наполняют этап Selections воронки dev-stats. Пустая подборка событий item-added не даёт (A2 отсекает пустое избранное).
  8. Результат. SelectionResult: SelectionId, Title, PublicCode, Source = Favorites, ItemsCount, SkippedCount (0, если избранное ≤ 200), CreatedAt.
  9. Фронт переходит на страницу подборки /collection (детали), показывает состав; display-данные объектов недвижимости добирает батчем в catalog по PropertyObjectId. При SkippedCount > 0 — предупреждение «в подборку вошли первые 200 объектов недвижимости».
  10. Consumer user-activity получает selection-created → ActionEvent «создал подборку» (далее — карточка лида CRM, ADR-0054).

Альтернативные Потоки

A1. favorites недоступен

  1. Internal-вызов favorites упал (timeout/5xx) → SELECTIONS_FAVORITES_UNAVAILABLE (503); подборка не создаётся (частичных состояний нет — до записи дело не дошло).
  2. Фронт предлагает повторить позже; создание вручную (POST /selections) продолжает работать (../README.md «Зависимости», failure mode). Завершается.

A2. Избранное пусто

  1. Internal API вернул пустой список → SELECTIONS_FAVORITES_EMPTY (422).
  2. Фронт скрывает/дизейблит кнопку заранее (знает счётчик из GET /favorites); при гонке показывает «в избранном нет объектов недвижимости». Пустая подборка из избранного не создаётся (осмысленного контента нет); пустую подборку можно создать только вручную и наполнить позже. Завершается.

A3. В избранном больше 200 объектов недвижимости (INV-SEL-004)

  1. Берутся первые 200 по createdAt desc (самые свежие лайки); SkippedCount = N - 200 возвращается в ответе.
  2. Фронт показывает предупреждение и предлагает отредактировать состав (PUT /selections/{id}/items). Операция не блокируется целиком — деградация вместо отказа. Возврат к основному потоку (переход на страницу подборки).

A4. Коллизия PublicCode

  1. Вставка нарушила unique-индекс ux_selections_public_code (вероятность ~2^-71 на код — практически недостижимо) → регенерация кода и повтор создания, до 3 попыток (INV-SEL-005); после — 500. Возврат к основному потоку (создание подборки).

A5. Достигнут лимит подборок владельца

  1. Проверка лимита: 100 активных подборок → SELECTIONS_LIMIT_EXCEEDED (422).
  2. Фронт предлагает удалить неиспользуемые подборки. Завершается.

A6. Создание подборки вручную (смежная операция)

  1. POST /selections { title, description?, propertyObjectIds?: Guid[] (0..200) } — тот же сценарий без шага чтения избранного (состав из формы, дедупликация INV-SEL-003 на сервере), Source = Manual, Comment = null у всех элементов. Дом операции — ../api/public/create-selection.md (будущий путь). Завершается.

Постусловие

  • Создан агрегат Selection (Source = Favorites, уникальный неизменяемый PublicCode) с элементами: состав = снимок избранного на момент вызова (≤ 200, SortOrder 0..N-1), комментарии — копии заметок при copyNotes = true.
  • Подборка независима от избранного (INV-SEL-010): последующие лайки/дизлайки/правки заметок её не меняют; правки подборки не трогают избранное.
  • Опубликовано pin.selections.selection-created.v1 (+ по одному selection-item-added.v1 на каждый скопированный объект недвижимости — атомарно с созданием). ActionEvent «создал подборку» и этап воронки dev-stats появляются асинхронно (окно — SLA консюмеров: user-activity < 1 мин, dev-stats < 5 мин); сама подборка и её публичная ссылка доступны сразу после ответа 201 — промежуточное состояние «подборка есть, карточки лида ещё без отметки» ожидаемо.
  • Подборка сразу доступна по публичной ссылке /collection/{publicCode} (UC-SEL-003).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400title пуст/длиннее 256; description > 2000Подсветить поля формы
401401Нет JWT (гость)Открыть auth-модалку
SELECTIONS_LIMIT_EXCEEDED422≥ 100 активных подборок (INV-SEL-007, A5)Предложить удалить старые
SELECTIONS_FAVORITES_EMPTY422Избранное пусто (A2)Скрыть кнопку / показать пустое состояние
SELECTIONS_FAVORITES_UNAVAILABLE503favorites недоступен (A1)Повторить позже; предложить ручное создание
5xx500Исчерпаны попытки регенерации PublicCode (A4) / инфраструктураRetry с backoff

Доменные События

СобытиеКогда поднимаетсяSubscribers
SelectionCreatedEvent (SelectionId, OwnerUserId, Source, ItemsCount)Успешное создание подборкиВнутренние (метрика создания по Source)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
SelectionCreatedKafkaEvent (ContractVersion = 1, AggregateId = SelectionId)pin.selections.selection-created.v1Создание подборки (Manual и Favorites — одно событие, различие в поле Source)user-activity (ActionEvent → карточка лида CRM)
SelectionItemAddedKafkaEvent (ContractVersion = 1, key = SelectionId)pin.selections.selection-item-added.v1По одному на каждый скопированный объект недвижимости (M событий, атомарно с созданием)dev-stats (этап Selections воронки), user-activity (AddToSelection)

Acceptance Criteria

#КритерийПроверка
AC1Состав созданной подборки = избранное владельца на момент вызова, порядок — createdAt desc, SortOrder 0..N-1интеграционный тест с mock favorites
AC2copyNotes = false (default): все Comment = null; copyNotes = true: Comment = Note избранногопараметризованный тест флага
AC3Последующее удаление объекта недвижимости из избранного не меняет подборку; правка подборки не меняет избранное (INV-SEL-010)сквозной тест независимости
AC4250 объектов недвижимости в избранном → 200 элементов + SkippedCount = 50 (A3)интеграционный тест лимита
AC5Пустое избранное → 422 SELECTIONS_FAVORITES_EMPTY, подборка не создана (A2)негативный тест
AC6favorites недоступен → 503, никаких строк в selections.* (A1)fault-injection тест
AC7100-я активная подборка создаётся, 101-я → SELECTIONS_LIMIT_EXCEEDED; soft-deleted не считаютсятест границы INV-SEL-007
AC8PublicCode уникален, 12 base62, не меняется при последующих правках (INV-SEL-005)контрактный тест
AC9Гость (без JWT) → 401негативный тест
AC10Создание из избранного с M объектами публикует ровно M событий selection-item-added.v1 (по одному SelectionItemId) + один selection-created.v1, все атомарноинтеграционный тест эмиссии событий

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы