UC-SEL-001. Создание подборки из текущего избранного
Назначение
Описывает сценарий создания подборки из текущего избранного владельца: сервер читает состав
избранного пользователя через internal API favorites и создаёт независимую подборку (одноразовое
копирование, INV-SEL-010 — дальнейшие изменения избранного подборку не меняют и наоборот). Заметки
избранного копируются в комментарии элементов только по явному флагу copyNotes (заметка приватна,
комментарий подборки публичен). Экран — страница /collection (подборки из избранного). Связан с
контекстом selections.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-SEL-001 |
| Контекст | selections (см. docs/06-services/selections/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор или Агент (авторизованный пользователь). Гость подборки не создаёт — подборка привязана к владельцу-пользователю (../README.md «Известные Ограничения») |
| Permissions / scope | self-scope владельца (JWT, claim sub); отдельных permissions нет (INV-SEL-002) |
| Tenant | не tenanted (ADR-0052 п.2 — подборка личная) |
| Record-level | OwnerUserId = 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 → «Новая подборка → из избранного» |
Основной Поток
- Пользователь нажимает «Создать подборку из избранного», вводит название (и опционально описание), выбирает флаг «Перенести мои заметки в комментарии подборки» (по умолчанию выключен — фронт предупреждает, что комментарии будут видны всем, кто откроет ссылку).
- Фронт отправляет
POST /selections/from-favoritesс теломCreateSelectionFromFavoritesForm { title: string (1..256 после trim), description?: string (≤ 2000), copyNotes: bool = false }(JWT владельца). - Проверка входа и лимита.
title— 1..256 после trim,description— ≤ 2000; нарушение →ValidationError(400). У владельца должно быть < 100 не удалённых подборок (INV-SEL-007) — иначеSELECTIONS_LIMIT_EXCEEDED(422, поток A5). Существование объектов недвижимости не проверяется (INV-SEL-008). Повторный вызов сознательно создаёт новую подборку (пользователь может снимать «снимки» избранного многократно); защита от двойного клика — на фронте (disable кнопки). - Чтение избранного. Синхронный 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 всё равно отвязывает копию). - Формирование состава. Избранное упорядочивается по
createdAtdesc (новые лайки — первыми). Если элементов > 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). - Создание подборки. Создаётся подборка
Source = Favoritesс уникальным неизменяемымPublicCode(12 символов base62, CSPRNG — INV-SEL-005; коллизия unique-индексаux_selections_public_codeрегенерируется, до 3 попыток — поток A4). Подборка и её элементы создаются в одном согласованном изменении; в том же изменении атомарно фиксируется факт создания (внутренний доменный фактSelectionCreatedEvent). - Публикация факта. В той же атомарной фиксации публикуются:
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 отсекает пустое избранное). - Результат.
SelectionResult:SelectionId,Title,PublicCode,Source = Favorites,ItemsCount,SkippedCount(0, если избранное ≤ 200),CreatedAt. - Фронт переходит на страницу подборки
/collection(детали), показывает состав; display-данные объектов недвижимости добирает батчем вcatalogпоPropertyObjectId. ПриSkippedCount > 0— предупреждение «в подборку вошли первые 200 объектов недвижимости». - Consumer
user-activityполучаетselection-created→ ActionEvent «создал подборку» (далее — карточка лида CRM, ADR-0054).
Альтернативные Потоки
A1. favorites недоступен
- Internal-вызов
favoritesупал (timeout/5xx) →SELECTIONS_FAVORITES_UNAVAILABLE(503); подборка не создаётся (частичных состояний нет — до записи дело не дошло). - Фронт предлагает повторить позже; создание вручную (
POST /selections) продолжает работать (../README.md «Зависимости», failure mode). Завершается.
A2. Избранное пусто
- Internal API вернул пустой список →
SELECTIONS_FAVORITES_EMPTY(422). - Фронт скрывает/дизейблит кнопку заранее (знает счётчик из
GET /favorites); при гонке показывает «в избранном нет объектов недвижимости». Пустая подборка из избранного не создаётся (осмысленного контента нет); пустую подборку можно создать только вручную и наполнить позже. Завершается.
A3. В избранном больше 200 объектов недвижимости (INV-SEL-004)
- Берутся первые 200 по
createdAtdesc (самые свежие лайки);SkippedCount = N - 200возвращается в ответе. - Фронт показывает предупреждение и предлагает отредактировать состав (
PUT /selections/{id}/items). Операция не блокируется целиком — деградация вместо отказа. Возврат к основному потоку (переход на страницу подборки).
A4. Коллизия PublicCode
- Вставка нарушила unique-индекс
ux_selections_public_code(вероятность ~2^-71 на код — практически недостижимо) → регенерация кода и повтор создания, до 3 попыток (INV-SEL-005); после — 500. Возврат к основному потоку (создание подборки).
A5. Достигнут лимит подборок владельца
- Проверка лимита: 100 активных подборок →
SELECTIONS_LIMIT_EXCEEDED(422). - Фронт предлагает удалить неиспользуемые подборки. Завершается.
A6. Создание подборки вручную (смежная операция)
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,SortOrder0..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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | title пуст/длиннее 256; description > 2000 | Подсветить поля формы |
401 | 401 | Нет JWT (гость) | Открыть auth-модалку |
SELECTIONS_LIMIT_EXCEEDED | 422 | ≥ 100 активных подборок (INV-SEL-007, A5) | Предложить удалить старые |
SELECTIONS_FAVORITES_EMPTY | 422 | Избранное пусто (A2) | Скрыть кнопку / показать пустое состояние |
SELECTIONS_FAVORITES_UNAVAILABLE | 503 | favorites недоступен (A1) | Повторить позже; предложить ручное создание |
5xx | 500 | Исчерпаны попытки регенерации 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 |
| AC2 | copyNotes = false (default): все Comment = null; copyNotes = true: Comment = Note избранного | параметризованный тест флага |
| AC3 | Последующее удаление объекта недвижимости из избранного не меняет подборку; правка подборки не меняет избранное (INV-SEL-010) | сквозной тест независимости |
| AC4 | 250 объектов недвижимости в избранном → 200 элементов + SkippedCount = 50 (A3) | интеграционный тест лимита |
| AC5 | Пустое избранное → 422 SELECTIONS_FAVORITES_EMPTY, подборка не создана (A2) | негативный тест |
| AC6 | favorites недоступен → 503, никаких строк в selections.* (A1) | fault-injection тест |
| AC7 | 100-я активная подборка создаётся, 101-я → SELECTIONS_LIMIT_EXCEEDED; soft-deleted не считаются | тест границы INV-SEL-007 |
| AC8 | PublicCode уникален, 12 base62, не меняется при последующих правках (INV-SEL-005) | контрактный тест |
| AC9 | Гость (без JWT) → 401 | негативный тест |
| AC10 | Создание из избранного с M объектами публикует ровно M событий selection-item-added.v1 (по одному SelectionItemId) + один selection-created.v1, все атомарно | интеграционный тест эмиссии событий |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-SEL-002. Шаринг подборки по ссылке (SharePopover + реферальная ссылка), UC-SEL-005. Агент отправляет подборку клиенту в лиде/сделке
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы:
../api/public/create-selection-from-favorites.md,../api/public/create-selection.md,../api/public/set-selection-items.md(будущие пути по конвенции, api-method-template). - Domain entity: ../domain/selection.md — дом полей,
SelectionSource, INV-SEL-002..010. - Источник состава: ../../favorites/README.md (internal API),
../../favorites/domain/favorite.md (
Note). - Следующие сценарии: UC-SEL-002 (шаринг), UC-SEL-003 (открытие по ссылке).
- Решения: ADR-0052, ADR-0056.