POST /api/v1/selections/from-favorites — Создать подборку из текущего избранного
Одноразовое копирование состава избранного владельца (контекст favorites) в новую подборку:
объекты недвижимости + порядок + (по явному флагу) личные заметки → комментарии элементов. После создания подборка
живёт независимо от избранного (INV-SEL-010).
Назначение
Пользователь нажимает «Создать подборку из избранного». Сервер читает актуальный список избранного
через internal API favorites и переносит его в новую подборку c Source = Favorites. Заметки
избранного приватны — они копируются в публичные комментарии подборки только по явному флагу
copyNotes (фронт предупреждает). Дом полей — ../domain/selection.md;
источник — ../../favorites/domain/favorite.md.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | selections |
| API area | public |
| Feature group | selections |
| Статус | draft |
| Документ | docs/06-services/selections/api/public/create-selection-from-favorites.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (JWT). Гость исключён дважды: подборка требует владельца, а гостевое избранное (AnonymousId) в подборку не читается — сначала вход и слияние (../../favorites/domain/merge-strategy.md) |
| Auth policy / Permissions | authenticated user; отдельных permissions нет (self-scope) |
| Scope (RBAC) | self: читается только избранное userId = actor.UserId |
| Record-level | OwnerUserId := actor.UserId; избранное запрашивается строго по актору (INV-SEL-002, INV-FAV-005) |
| Tenant isolation | не применяется — вне tenant (ADR-0052 п.2) |
| Audit | create: SelectionCreatedKafkaEvent (Source = Favorites) → user-activity; SelectionItemAddedKafkaEvent (per copied item) → dev-stats (воронка) + user-activity (AddToSelection) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/selections/from-favorites |
| Success status | 201 (Location: /api/v1/selections/{publicCode}) |
| Idempotency header | не применяется (повтор создаёт вторую независимую копию — допустимо by design; лимит INV-SEL-007) |
| Correlation | сквозной trace через заголовок traceparent, в т.ч. в вызов favorites |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | владелец и субъект чтения избранного — только actor |
Title | body | string | Да | 1..256 после trim | — |
Description | body | string? | Нет | ≤ 2000 после trim; пустая строка → null | — |
CopyNotes | body | bool | Нет (default false) | — | true переносит приватные заметки избранного в публичные комментарии подборки — фронт показывает явное предупреждение |
Модель ответа CreateSelectionResult
Та же модель, что у create-selection.md (Id, PublicCode, Title,
Source = "Favorites", ItemsCount, CreatedAt) — дельта: Source всегда Favorites.
Контракт internal API favorites (upstream, assumption)
GET /internal/favorites?userId={userId} (m2m selections, будущий док
../../favorites/api/internal/list-favorites.md) —
возвращает элементы: PropertyObjectId : Guid, Note : string? (≤ 4000), CreatedAt : DateTimeOffset,
отсортированные CreatedAt desc. Таймаут клиента 3 с, 1 retry на сетевые/5xx ошибки.
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный пользователь (владелец); читается только его собственное избранное (по актору). Гость исключён — нужен владелец и слияние гостевого избранного при входе.
- Проверка входа и предусловий.
Title1..256,Description≤ 2000 — иначеVALIDATION_ERROR. Лимит владельца: 100 не удалённых подборок (INV-SEL-007) → отказSELECTIONS_LIMIT_EXCEEDED. Существование объектов недвижимости не проверяется (INV-SEL-008 —PropertyObjectIdприходят изfavorites, где действует симметричный INV-FAV-004). - Что читаем. Актуальный список избранного владельца — синхронным чтением internal API
favorites(единственный внешний вызов; сбой/таймаут →SELECTIONS_FAVORITES_UNAVAILABLE). Порядок —CreatedAtdesc (как на странице избранного). Проверки результата:- избранное пусто →
SELECTIONS_FAVORITES_EMPTY(фронт предлагает добавить объекты); - в избранном > 200 объектов (INV-SEL-004) →
SELECTIONS_ITEMS_LIMIT_EXCEEDED(состав молча не усекаем; фронт предлагает собрать подборку вручную из части избранного).
- избранное пусто →
- Бизнес-правила и маппинг. Сгенерировать
PublicCode(12 base62, INV-SEL-005). Создать подборку сSource = Favorites, владелец — актор. Перенести объекты недвижимости и заметки: для каждого элемента избранного в порядкеCreatedAtdesc —SortOrder= 0..N-1 (INV-SEL-006),Comment= заметка избранного только приCopyNotes = true(иначе пусто). Дубли невозможны (вfavoritesуникальность «владелец+объект», INV-FAV-002), состав всё равно дедуплицируется (INV-SEL-003); длинаComment≤ 4000 гарантирована источником (INV-FAV-003). На каждый добавленный элемент растёт счётчикVersionи фиксируется факт добавления. Копия одноразовая: подписок/синхронизации нет — дальнейшие изменения избранного подборку не меняют и наоборот (INV-SEL-010). Фиксируется факт создания подборки. - Что меняется. Появляется подборка с элементами (в одном согласованном изменении). Записи
favoritesне изменяются (read-only интеграция). Чтениеfavoritesвыполняется до фиксации — внешних вызовов внутри согласованного изменения нет. При коллизииPublicCode— регенерация, ≤ 3 попыток. - Результат. Идентификатор,
PublicCode,Title,Source = Favorites, число элементов, дата создания →201+Location. Состав зафиксирован на момент ответаfavorites(шаг 3): добавление лайка после этого момента в подборку не попадает — приемлемо по INV-SEL-010 (копия «на момент создания»).
Публикуемые интеграционные события (создание + по одному на каждый скопированный объект) — в разделе «Побочные эффекты».
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | SelectionCreatedKafkaEvent / pin.selections.selection-created.v1 / key = SelectionId + SelectionItemAddedKafkaEvent / pin.selections.selection-item-added.v1 / key = SelectionId — по одному на каждый скопированный объект недвижимости |
| Внешние вызовы | чтение избранного — internal API favorites, m2m client-credentials, timeout 3 с, 1 retry (идемпотентный GET); вне согласованного изменения |
Интеграционные последовательности
Синхронная фаза (до ответа клиенту):
- Клиент →
selections:POST /from-favorites(JWT). selections→favorites:GET /internal/favorites?userId={actor}(m2m, сквозной trace, timeout 3 с, 1 retry).favoritesпредоставляет элементы (PropertyObjectId,Note?,CreatedAt) в порядкеCreatedAtdesc; гарантирует уникальность «владелец+объект» (INV-FAV-002) иNote ≤ 4000(INV-FAV-003). При недоступности/таймауте после retry →SELECTIONS_FAVORITES_UNAVAILABLE(503), подборка не создаётся, деградация изолирована (остальные операцииselectionsработают).selectionsфиксирует подборку + элементы в одном согласованном изменении и отвечает201.
Асинхронная фаза (через outbox, key = SelectionId):
selection-created(Source = Favorites) →user-activity: ActionEvent «создал подборку»; для владельца-агента питает карточку лида CRM (ADR-0054).selection-item-added— по одному на каждый скопированный объект (несёт монотонныйVersion) →dev-stats(этапSelectionsворонки) иuser-activity(AddToSelection).- Окно eventual-consistency: воронка/история догоняют за секунды; подборка и её публичная ссылка
функциональны сразу. Потребители идемпотентны по (
SelectionId,SelectionItemId/Version).
Копия одноразовая (INV-SEL-010): подписок на изменения избранного нет — обратной интеграции
favorites → selections не существует, дальнейшие лайки/анлайки подборку не меняют.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_ERROR | 400 | Title/Description вне ограничений | «Некорректные данные подборки» | no retry |
SELECTIONS_LIMIT_EXCEEDED | 400 | 100 подборок у владельца (INV-SEL-007) | «Достигнут лимит подборок (100)» | no retry |
SELECTIONS_FAVORITES_EMPTY | 400 | Избранное пусто | «В избранном нет объектов» | no retry |
SELECTIONS_ITEMS_LIMIT_EXCEEDED | 400 | В избранном > 200 объектов недвижимости (INV-SEL-004) | «В подборку помещается не более 200 объектов» | no retry |
SELECTIONS_FAVORITES_UNAVAILABLE | 503 | favorites недоступен/таймаут после retry | «Сервис избранного временно недоступен» | retry позже |
UNAUTHORIZED | 401 | Нет/просрочен access-токен | — | retry после refresh |
Предусловия и постусловия
- Предусловия: актор авторизован (валидный JWT; гость исключён — нужны владелец и слияние гостевого
избранного при входе); у владельца < 100 не удалённых подборок (INV-SEL-007); сервис
favoritesдоступен; в избранном актора 1..200 объектов. - Постусловия при
201: существует новая подборкаSource = Favoritesс уникальным неизменяемымPublicCode(INV-SEL-005) и элементами-копией избранного на момент чтения (шаг 3 алгоритма);SortOrder= 0..N-1 в порядкеCreatedAtdesc источника (INV-SEL-006);Commentзаполнен из заметок избранного только приCopyNotes = true, иначе пуст; счётчик не удалённых подборок владельца +1; в outbox поставленыselection-createdи по одномуselection-item-addedна объект. Записиfavoritesне изменены (read-only интеграция), подписки на синхронизацию не созданы (INV-SEL-010). - Постусловия при отказе (400/401/503): подборка не создана,
PublicCodeне выделен, событий нет; избранное не изменено.
Acceptance Criteria
- Given авторизованный владелец с 3 объектами в избранном (без заметок), When
POSTсTitle = "Из избранного", Then201,Source = Favorites,ItemsCount = 3,SortOrderпоCreatedAtdesc; поставленыselection-created+ 3×selection-item-added(key =SelectionId). - Given избранное с заметками, When
POSTсcopyNotes = true, Then заметки перенесены в публичныеCommentэлементов; WhencopyNotesопущен/false, ThenCommentэлементов пусты (заметки не раскрываются). - Given пустое избранное, When
POST, Then400 SELECTIONS_FAVORITES_EMPTY, подборка не создана. - Given избранное с 201 объектом, When
POST, Then400 SELECTIONS_ITEMS_LIMIT_EXCEEDED(INV-SEL-004); состав молча не усекается. - Given владелец уже со 100 не удалёнными подборками, When
POST, Then400 SELECTIONS_LIMIT_EXCEEDED(INV-SEL-007). - Given
favoritesнедоступен/таймаутит после retry, WhenPOST, Then503 SELECTIONS_FAVORITES_UNAVAILABLE; клиент повторяет позже; прочие операцииselectionsработают. - Given подборка создана, When пользователь после ответа добавил новый лайк в избранное, Then он не попадает в подборку (копия «на момент создания», INV-SEL-010).
- Given
Titleиз одних пробелов, WhenPOST, Then400 VALIDATION_ERROR. - Given отсутствующий/просроченный токен, When
POST, Then401 UNAUTHORIZED.
Совместимость и наблюдаемость
- Контракт internal API
favorites— assumption до утвержденияfavorites/api/internal/list-favorites.md; additive-изменения ответаfavoritesбезопасны (unknown-поля игнорируются). - Logs:
UserId,SelectionId,ItemsCount,CopyNotes(без заметок и списка объектов недвижимости); metrics:selections_created_total{source="favorites"},selections_from_favorites_upstream_errors_total; traces: span команды + child-span HTTP-вызоваfavorites. Dashboard —selections-overview; runbook: рост 503 → проверитьfavorites(health), операция деградирует изолированно.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): API контекста selections — индекс, POST /api/v1/selections — Создать подборку вручную
- События (1): Event Contract: pin.selections.selection-item-added (SelectionItemAddedKafkaEvent)
- Консюмеры (1): Consumer: DevStatsFunnelSelectionItemAddedConsumer
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/selection.md (INV-SEL-004..010); источник: ../../favorites/domain/favorite.md (INV-FAV-002/003/005), ../../favorites/README.md (internal-область).
- Соседние: create-selection.md, list-my-selections.md.