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

POST /api/v1/selections — Создать подборку вручную

Создание пустой или предзаполненной подборки владельцем: название, описание, опциональный начальный список объектов недвижимости. Генерируется неизменяемый PublicCode публичной ссылки (INV-SEL-005).

Назначение

Владелец (инвестор или агент) создаёт подборку с нуля — например, агент собирает объекты недвижимости под запрос клиента. Создание «из текущего избранного» — отдельная операция create-selection-from-favorites.md. Дом полей — ../domain/selection.md.

Metadata

ПолеЗначение
Сервис/контекстselections
API areapublic
Feature groupselections
Статусdraft
Документdocs/06-services/selections/api/public/create-selection.md

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

ПроверкаЗначение
Actor typeUser (JWT; гость подборки не создаёт — подборка привязана к владельцу)
Auth policy / Permissionsauthenticated user; отдельных permissions нет (self-scope)
Scope (RBAC)self: OwnerUserId := actor.UserId, id владельца из запроса не принимается
Record-levelновая запись создаётся строго на актора (INV-SEL-002)
Tenant isolationне применяется — вне tenant (ADR-0052 п.2)
Auditcreate: CreatedAt + SelectionCreatedKafkaEventuser-activity

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredвладелец — только actor
TitlebodystringДа1..256 символов после trim (дом: Selection.Title)
Descriptionbodystring?Нет≤ 2000 после trim; пустая строка → null
PropertyObjectIdsbodyIReadOnlyList<Guid>?Нет≤ 200 (INV-SEL-004); валидные непустые Guid; дубли схлопываются (INV-SEL-003); существование объектов недвижимости не проверяется (INV-SEL-008)untrusted input

Модель ответа CreateSelectionResult

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаSelection.IdИдентификатор созданной подборки.
PublicCodestringДаSelection.PublicCodeКод публичной ссылки (12 base62), сразу пригоден для шаринга.
TitlestringДаSelection.TitleНазвание после trim.
Sourcestring (enum SelectionSource)ДаSelection.Source = ManualСпособ создания.
ItemsCountintДаItems.Count после дедупликацииЧисло элементов (0..200).
CreatedAtDateTimeOffsetДаSelection.CreatedAtМомент создания.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (владелец); id владельца берётся из токена, из запроса не принимается. Повтор создаёт вторую подборку (идемпотентности по ключу нет — фронт дизейблит кнопку, ущерб ограничен лимитом INV-SEL-007).
  2. Проверка входа и предусловий. Title 1..256 после trim; Description ≤ 2000; каждый PropertyObjectId непустой, всего ≤ 200 (INV-SEL-004), дубли схлопываются (INV-SEL-003) — иначе VALIDATION_ERROR. Лимит владельца: если у него уже 100 не удалённых подборок (INV-SEL-007) → отказ SELECTIONS_LIMIT_EXCEEDED. Существование объектов недвижимости не проверяется (INV-SEL-008, ADR-0056).
  3. Бизнес-правила и маппинг. Сгенерировать неизменяемый PublicCode (12 base62, CSPRNG, INV-SEL-005). Создать подборку с Source = Manual, владелец — актор, Title/Description после trim. Элементы добавляются в порядке запроса: SortOrder = 0..N-1 (INV-SEL-006), дубли отфильтрованы (INV-SEL-003). Фиксируется факт создания подборки.
  4. Что меняется. Появляется подборка с элементами (в одном согласованном изменении). При крайне маловероятной коллизии PublicCode (unique) код регенерируется, до 3 попыток (INV-SEL-005), затем инфраструктурная ошибка 500.
  5. Результат. Идентификатор, PublicCode, Title, Source, число элементов, дата создания → 201
    • заголовок Location. Masking не требуется.

Публикуемое интеграционное событие — в разделе «Побочные эффекты».

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияSelectionCreatedKafkaEvent / pin.selections.selection-created.v1 / key = SelectionId (одно на создание)

При ручном создании поэлементные selection-item-added не публикуются даже с непустым начальным списком — начальный состав отражён только агрегатным ItemsCount в selection-created. Поэлементная гранулярность (нужная воронке dev-stats, этап Selections) возникает в операциях from-favorites (create-selection-from-favorites.md) и замены состава (set-selection-items.md).

Интеграционные последствия (асинхронно, через outbox)

Событие уходит через outbox после коммита подборки (at-least-once, key = SelectionId — порядок событий одной подборки сохраняется). Цепочка:

  1. selection-createduser-activity: фиксирует ActionEvent «создал подборку» в истории действий владельца; если владелец-агент, событие питает карточку лида CRM (ADR-0054).
  2. Окно eventual-consistency: между 201 и появлением записи в истории действий — секунды (лаг outbox
    • консюмер). В интерфейсе владельца подборка видна сразу (read-your-writes из основной БД, list-my-selections.md); лента действий догоняет.
  3. При простое консюмера/шины подборка уже создана и функциональна (ссылка работает); отстаёт только история действий — потребитель идемпотентен по SelectionId (повторная доставка не дублирует запись).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Title/Description/PropertyObjectIds вне ограничений«Некорректные данные подборки»no retry
SELECTIONS_LIMIT_EXCEEDED400У владельца уже 100 не удалённых подборок (INV-SEL-007)«Достигнут лимит подборок (100)»no retry
UNAUTHORIZED401Нет/просрочен access-токенretry после refresh

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

  • Предусловия: актор авторизован (валидный JWT); у владельца < 100 не удалённых подборок (INV-SEL-007).
  • Постусловия при 201: существует ровно одна новая подборка Source = Manual с уникальным неизменяемым PublicCode (INV-SEL-005) и ≤ 200 элементами, чьи SortOrder образуют плотную последовательность 0..N-1 без дублей PropertyObjectId (INV-SEL-003/006); счётчик не удалённых подборок владельца увеличился на 1; в outbox поставлено событие selection-created.
  • Постусловия при любом отказе (400/401): подборка не создана, PublicCode не выделен, событий нет.

Acceptance Criteria

  • Given владелец с 5 подборками, When POST с Title = "Для Ивановых" и propertyObjectIds = [A, B, C] (уникальные), Then 201 с Location, ItemsCount = 3, Source = Manual, SortOrder A=0/B=1/C=2, выдан PublicCode (12 base62); поставлено selection-created (key = SelectionId).
  • Given тот же владелец, When propertyObjectIds = [A, B, A], Then дубль A схлопнут → ItemsCount = 2 (INV-SEL-003), 201.
  • Given владелец без начального списка, When propertyObjectIds пуст или отсутствует, Then 201, ItemsCount = 0 (пустая подборка допустима).
  • Given Title из одних пробелов (после trim пусто), When POST, Then 400 VALIDATION_ERROR, подборка не создана.
  • Given propertyObjectIds содержит Guid.Empty (или невалидный Guid), When POST, Then 400 VALIDATION_ERROR (INV-SEL-008 проверяет формат, не существование), подборка не создана.
  • Given propertyObjectIds из 201 элемента, When POST, Then 400 VALIDATION_ERROR (INV-SEL-004).
  • Given крайне маловероятная коллизия сгенерированного PublicCode с существующим, When POST, Then код регенерируется (до 3 попыток, INV-SEL-005) и подборка создаётся с уникальным кодом; при исчерпании попыток — инфраструктурная 500 (подборка не создана).
  • Given владелец уже со 100 не удалёнными подборками, When POST, Then 400 SELECTIONS_LIMIT_EXCEEDED (INV-SEL-007), 101-я не создаётся.
  • Given отсутствующий/просроченный токен, When POST, Then 401 UNAUTHORIZED.
  • Given две одинаковые быстрые отправки формы, When обе доходят, Then создаются две подборки с разными PublicCode (идемпотентности по ключу нет; ущерб ограничен лимитом INV-SEL-007).

Совместимость и наблюдаемость

  • Новые optional поля запроса/ответа — additive; прочее — breaking (migration + rollout).
  • Logs: UserId, SelectionId, ItemsCount (без Title и списка объектов недвижимости); metrics: selections_created_total{source="manual"}, гистограмма длительности; traces: span приёма запроса → span бизнес-обработки → span записи в хранилище. Dashboard — selections-overview; alert на outbox-lag — ../README.md.

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

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

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