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

GET /api/v1/selections/{publicCode} — Открыть подборку по публичной ссылке

Публичное чтение подборки по коду PublicCode: любой актор, включая неавторизованного гостя. Ссылку /collection/{publicCode} рассылает владелец, при необходимости с реф-параметром.

Назначение

Получатель ссылки (клиент агента, знакомый инвестора) открывает подборку без входа: название, описание, упорядоченный список объектов недвижимости с комментариями владельца. Display-данные объектов недвижимости (цена, фото, название) фронт добирает батчем в catalog — этот метод отдаёт только состав. Дом полей — ../domain/selection.md.

Гейт гостя

Состав подборки отдаётся по коду любому актору, но коммерческие карточки объектов недвижимости остаются под гейтом каталога:

СлойПравило
selections (этот метод)Отдаёт состав (PropertyObjectId, комментарии, название) любому актору с валидным кодом — сам код (12 base62, ~71 бит, непереборный) и есть капабилити доступа. Rate-limit на 404 против сканирования кодов
catalog (батч display-данных)Применяет свой гейт: JWT или реф-cookie (30 дней)
Клиент /collection/{publicCode}Ссылка владельца несёт реф-параметр (referral); при открытии он устанавливает cookie → гость проходит гейт catalog и видит карточки. Гость без cookie видит состав подборки и CTA входа вместо карточек

Так подборка открывается кем угодно, а коммерческие данные объектов недвижимости остаются под гейтом каталога; реферальная атрибуция — зона referral (docs/06-services/referral/README.md).

Metadata

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

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

ПроверкаЗначение
Actor typeAnonymous или User (JWT опционален; используется только для вычисления IsOwner)
Auth policy / Permissionsnone (публичная операция)
Scope (RBAC)не применяется
Record-levelне применяется к чтению (публичность by design); OwnerUserId в ответ не попадает
Tenant isolationне применяется — вне tenant (ADR-0052 п.2)
Auditnone в этом сервисе; просмотр публичной подборки трекает user-activity (фронт-событие)

Feature flag selections.public-open.enabled = false (аварийное закрытие при абьюзе) → метод отвечает 404 SELECTIONS_NOT_FOUND всем, включая владельца (владелец пользуется GET /api/v1/selections).

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/selections/{publicCode} (сегмент не-Guid; Guid уходит в маршруты {id:guid} — см. README.md)
Success status200
Idempotency headerне применяется (безопасный read)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
PublicCoderoutestringДаровно 12 символов [0-9A-Za-z]; иначе — сразу 404 без похода в БДuntrusted input; rate-limit 404 (см. гейт)
UserIdidentity claims (sub), опциональноGuid?Неттолько для IsOwner; отсутствие токена — валидный кейс

Body отсутствует.

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

ПолеТипОбязательностьИсточникMaskingОписание
PublicCodestringДаSelection.PublicCodeEcho кода (канонический регистр).
TitlestringДаSelection.TitleНазвание.
Descriptionstring?НетSelection.DescriptionОписание.
ItemsIReadOnlyList<PublicSelectionItemModel>Да (может быть пуст)Selection.Items order by SortOrderСостав.
IsOwnerboolДаcomputed: actor.UserId != null && actor.UserId == Selection.OwnerUserIdВладельцу фронт показывает кнопки редактирования (сами мутации — по Id через update-selection.md).
IdGuid?НетSelection.Id, только при IsOwner == trueскрыт для не-владельцаВнутренний id для мутаций владельца.
UpdatedAtDateTimeOffsetДаSelection.UpdatedAtСвежесть подборки.

OwnerUserId, ФИО владельца, Source, CreatedAt в публичный ответ не включаются (README: владелец наружу не раскрывается; подпись «подборка от…» — зона фронта владельца/news).

PublicSelectionItemModel

ПолеТипОбязательностьИсточникMaskingОписание
PropertyObjectIdGuidДаSelectionItem.PropertyObjectIdКлюч для батча display-данных в catalog.
SortOrderintДаSelectionItem.SortOrderПозиция 0..N-1.
Commentstring?НетSelectionItem.CommentКомментарий владельца — публичен by design (фронт предупреждает владельца при вводе).

Алгоритм

  1. Контекст и доступ. Операцию выполняет любой актор — аноним или авторизованный пользователь (токен нужен только для вычисления IsOwner).
  2. Проверка входа и предусловий. Если feature flag selections.public-open.enabled выключен → SELECTIONS_NOT_FOUND (без раскрытия причины). PublicCode — ровно 12 символов [0-9A-Za-z]; нарушение формата отвечает тем же SELECTIONS_NOT_FOUND (единый 404-контур против перебора).
  3. Что читаем. Подборка по PublicCode, не удалённая (DeletedAt == null), с элементами — горячий путь по partial-index ix_selections_public_code_active (SLO p99 < 150 мс). Нет кода или soft-deleted (INV-SEL-009) → SELECTIONS_NOT_FOUND.
  4. Бизнес-правила и маппинг. IsOwner = токен принадлежит владельцу подборки. Состав — элементы в порядке SortOrder; внутренний Id включается только владельцу. Существование объектов недвижимости не проверяется — недоступные пометит батч catalog на фронте (INV-SEL-008).
  5. Результат. Модель PublicSelectionQueryResult200. Masking: OwnerUserId исключён из модели; Id — conditional (шаг 4). Freshness: основная БД, read-your-writes для владельца.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
SELECTIONS_NOT_FOUND404Код не существует / формат неверен / подборка soft-deleted (INV-SEL-009) / флаг public-open выключен«Подборка не найдена»no retry
TOO_MANY_REQUESTS429Превышен rate-limit 404-ответов с одного IP (сканирование кодов)retry после Retry-After

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

  • Предусловия: feature flag selections.public-open.enabled включён; PublicCode — ровно 12 символов [0-9A-Za-z]. Авторизация не требуется (публичная операция); JWT опционален и влияет только на IsOwner/Id.
  • Постусловия: состояние не меняется (безопасное чтение); при успехе возвращается состав активной (DeletedAt == null) подборки без раскрытия владельца — OwnerUserId/ФИО/Source/CreatedAt в ответ не входят; внутренний Id присутствует только при IsOwner == true. Данные актуальны (основная БД, read-your-writes для владельца).

Acceptance Criteria

  • Given валидный код активной подборки, When гость (без JWT) открывает GET /selections/{publicCode}, Then 200 с Title/Description/Items (в порядке SortOrder), IsOwner = false, поле Id отсутствует; OwnerUserId не раскрыт.
  • Given тот же код, When открывает владелец (JWT его), Then 200, IsOwner = true, присутствует внутренний Id для кнопок редактирования.
  • Given код существует, но подборка soft-deleted (INV-SEL-009), When GET, Then 404 SELECTIONS_NOT_FOUND (то же, что для несуществующего кода — существование не раскрывается).
  • Given publicCode неверного формата (не 12 base62), When GET, Then 404 SELECTIONS_NOT_FOUND без обращения к БД (единый 404-контур против перебора).
  • Given feature flag selections.public-open.enabled = false, When GET (в т.ч. владельцем), Then 404 SELECTIONS_NOT_FOUND; владелец пользуется GET /api/v1/selections.
  • Given поток 404-ответов с одного IP превысил порог (сканирование кодов), When очередной GET, Then 429 TOO_MANY_REQUESTS с Retry-After.
  • Given гость без реф-cookie открыл подборку, When фронт добирает display-данные в catalog, Then состав подборки виден, но коммерческие карточки объектов под гейтом каталога → фронт показывает CTA входа вместо карточек (реф-ссылка владельца ставит cookie и снимает гейт).
  • Given объект недвижимости из состава недоступен/снят, When фронт добирает catalog, Then элемент помечается недоступным (существование не проверяется в selections, INV-SEL-008).

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

  • Новые optional поля ответа — additive; удаление/переименование — breaking. Формат PublicCode неизменяем (INV-SEL-005: разосланные ссылки не протухают).
  • Logs: PublicCode (не секрет после запроса), результат, аноним/юзер (без UserId для не-владельца); metrics: selections_public_open_total{result}, selections_public_open_404_rate (alert на всплеск — сканирование, ../README.md); traces: span GetSelectionByPublicCodeQuery. Dashboard — selections-overview; runbook — аварийное отключение флага selections.public-open.enabled.

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

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

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