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

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 areapublic
Feature groupselections
Статусdraft
Документdocs/06-services/selections/api/public/create-selection-from-favorites.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (JWT). Гость исключён дважды: подборка требует владельца, а гостевое избранное (AnonymousId) в подборку не читается — сначала вход и слияние (../../favorites/domain/merge-strategy.md)
Auth policy / Permissionsauthenticated user; отдельных permissions нет (self-scope)
Scope (RBAC)self: читается только избранное userId = actor.UserId
Record-levelOwnerUserId := actor.UserId; избранное запрашивается строго по актору (INV-SEL-002, INV-FAV-005)
Tenant isolationне применяется — вне tenant (ADR-0052 п.2)
Auditcreate: SelectionCreatedKafkaEvent (Source = Favorites) → user-activity; SelectionItemAddedKafkaEvent (per copied item) → dev-stats (воронка) + user-activity (AddToSelection)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/selections/from-favorites
Success status201 (Location: /api/v1/selections/{publicCode})
Idempotency headerне применяется (повтор создаёт вторую независимую копию — допустимо by design; лимит INV-SEL-007)
Correlationсквозной trace через заголовок traceparent, в т.ч. в вызов favorites

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredвладелец и субъект чтения избранного — только actor
TitlebodystringДа1..256 после trim
Descriptionbodystring?Нет≤ 2000 после trim; пустая строка → null
CopyNotesbodyboolНет (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 ошибки.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (владелец); читается только его собственное избранное (по актору). Гость исключён — нужен владелец и слияние гостевого избранного при входе.
  2. Проверка входа и предусловий. Title 1..256, Description ≤ 2000 — иначе VALIDATION_ERROR. Лимит владельца: 100 не удалённых подборок (INV-SEL-007) → отказ SELECTIONS_LIMIT_EXCEEDED. Существование объектов недвижимости не проверяется (INV-SEL-008 — PropertyObjectId приходят из favorites, где действует симметричный INV-FAV-004).
  3. Что читаем. Актуальный список избранного владельца — синхронным чтением internal API favorites (единственный внешний вызов; сбой/таймаут → SELECTIONS_FAVORITES_UNAVAILABLE). Порядок — CreatedAt desc (как на странице избранного). Проверки результата:
    • избранное пусто → SELECTIONS_FAVORITES_EMPTY (фронт предлагает добавить объекты);
    • в избранном > 200 объектов (INV-SEL-004) → SELECTIONS_ITEMS_LIMIT_EXCEEDED (состав молча не усекаем; фронт предлагает собрать подборку вручную из части избранного).
  4. Бизнес-правила и маппинг. Сгенерировать PublicCode (12 base62, INV-SEL-005). Создать подборку с Source = Favorites, владелец — актор. Перенести объекты недвижимости и заметки: для каждого элемента избранного в порядке CreatedAt desc — 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). Фиксируется факт создания подборки.
  5. Что меняется. Появляется подборка с элементами (в одном согласованном изменении). Записи favorites не изменяются (read-only интеграция). Чтение favorites выполняется до фиксации — внешних вызовов внутри согласованного изменения нет. При коллизии PublicCode — регенерация, ≤ 3 попыток.
  6. Результат. Идентификатор, 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); вне согласованного изменения

Интеграционные последовательности

Синхронная фаза (до ответа клиенту):

  1. Клиент → selections: POST /from-favorites (JWT).
  2. selectionsfavorites: GET /internal/favorites?userId={actor} (m2m, сквозной trace, timeout 3 с, 1 retry). favorites предоставляет элементы (PropertyObjectId, Note?, CreatedAt) в порядке CreatedAt desc; гарантирует уникальность «владелец+объект» (INV-FAV-002) и Note ≤ 4000 (INV-FAV-003). При недоступности/таймауте после retry → SELECTIONS_FAVORITES_UNAVAILABLE (503), подборка не создаётся, деградация изолирована (остальные операции selections работают).
  3. selections фиксирует подборку + элементы в одном согласованном изменении и отвечает 201.

Асинхронная фаза (через outbox, key = SelectionId):

  1. selection-created (Source = Favorites) → user-activity: ActionEvent «создал подборку»; для владельца-агента питает карточку лида CRM (ADR-0054).
  2. selection-item-added — по одному на каждый скопированный объект (несёт монотонный Version) → dev-stats (этап Selections воронки) и user-activity (AddToSelection).
  3. Окно eventual-consistency: воронка/история догоняют за секунды; подборка и её публичная ссылка функциональны сразу. Потребители идемпотентны по (SelectionId, SelectionItemId/Version).

Копия одноразовая (INV-SEL-010): подписок на изменения избранного нет — обратной интеграции favorites → selections не существует, дальнейшие лайки/анлайки подборку не меняют.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Title/Description вне ограничений«Некорректные данные подборки»no retry
SELECTIONS_LIMIT_EXCEEDED400100 подборок у владельца (INV-SEL-007)«Достигнут лимит подборок (100)»no retry
SELECTIONS_FAVORITES_EMPTY400Избранное пусто«В избранном нет объектов»no retry
SELECTIONS_ITEMS_LIMIT_EXCEEDED400В избранном > 200 объектов недвижимости (INV-SEL-004)«В подборку помещается не более 200 объектов»no retry
SELECTIONS_FAVORITES_UNAVAILABLE503favorites недоступен/таймаут после retry«Сервис избранного временно недоступен»retry позже
UNAUTHORIZED401Нет/просрочен access-токенretry после refresh

Предусловия и постусловия

  • Предусловия: актор авторизован (валидный JWT; гость исключён — нужны владелец и слияние гостевого избранного при входе); у владельца < 100 не удалённых подборок (INV-SEL-007); сервис favorites доступен; в избранном актора 1..200 объектов.
  • Постусловия при 201: существует новая подборка Source = Favorites с уникальным неизменяемым PublicCode (INV-SEL-005) и элементами-копией избранного на момент чтения (шаг 3 алгоритма); SortOrder = 0..N-1 в порядке CreatedAt desc источника (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 = "Из избранного", Then 201, Source = Favorites, ItemsCount = 3, SortOrder по CreatedAt desc; поставлены selection-created + 3× selection-item-added (key = SelectionId).
  • Given избранное с заметками, When POST с copyNotes = true, Then заметки перенесены в публичные Comment элементов; When copyNotes опущен/false, Then Comment элементов пусты (заметки не раскрываются).
  • Given пустое избранное, When POST, Then 400 SELECTIONS_FAVORITES_EMPTY, подборка не создана.
  • Given избранное с 201 объектом, When POST, Then 400 SELECTIONS_ITEMS_LIMIT_EXCEEDED (INV-SEL-004); состав молча не усекается.
  • Given владелец уже со 100 не удалёнными подборками, When POST, Then 400 SELECTIONS_LIMIT_EXCEEDED (INV-SEL-007).
  • Given favorites недоступен/таймаутит после retry, When POST, Then 503 SELECTIONS_FAVORITES_UNAVAILABLE; клиент повторяет позже; прочие операции selections работают.
  • Given подборка создана, When пользователь после ответа добавил новый лайк в избранное, Then он не попадает в подборку (копия «на момент создания», INV-SEL-010).
  • Given Title из одних пробелов, When POST, Then 400 VALIDATION_ERROR.
  • Given отсутствующий/просроченный токен, When POST, Then 401 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), операция деградирует изолированно.

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

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

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