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 area | public |
| Feature group | selections |
| Статус | draft |
| Документ | docs/06-services/selections/api/public/get-selection-by-code.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous или User (JWT опционален; используется только для вычисления IsOwner) |
| Auth policy / Permissions | none (публичная операция) |
| Scope (RBAC) | не применяется |
| Record-level | не применяется к чтению (публичность by design); OwnerUserId в ответ не попадает |
| Tenant isolation | не применяется — вне tenant (ADR-0052 п.2) |
| Audit | none в этом сервисе; просмотр публичной подборки трекает user-activity (фронт-событие) |
Feature flag selections.public-open.enabled = false (аварийное закрытие при абьюзе) → метод отвечает
404 SELECTIONS_NOT_FOUND всем, включая владельца (владелец пользуется GET /api/v1/selections).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/selections/{publicCode} (сегмент не-Guid; Guid уходит в маршруты {id:guid} — см. README.md) |
| Success status | 200 |
| Idempotency header | не применяется (безопасный read) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PublicCode | route | string | Да | ровно 12 символов [0-9A-Za-z]; иначе — сразу 404 без похода в БД | untrusted input; rate-limit 404 (см. гейт) |
UserId | identity claims (sub), опционально | Guid? | Нет | — | только для IsOwner; отсутствие токена — валидный кейс |
Body отсутствует.
Модель ответа PublicSelectionQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PublicCode | string | Да | Selection.PublicCode | — | Echo кода (канонический регистр). |
Title | string | Да | Selection.Title | — | Название. |
Description | string? | Нет | Selection.Description | — | Описание. |
Items | IReadOnlyList<PublicSelectionItemModel> | Да (может быть пуст) | Selection.Items order by SortOrder | — | Состав. |
IsOwner | bool | Да | computed: actor.UserId != null && actor.UserId == Selection.OwnerUserId | — | Владельцу фронт показывает кнопки редактирования (сами мутации — по Id через update-selection.md). |
Id | Guid? | Нет | Selection.Id, только при IsOwner == true | скрыт для не-владельца | Внутренний id для мутаций владельца. |
UpdatedAt | DateTimeOffset | Да | Selection.UpdatedAt | — | Свежесть подборки. |
OwnerUserId, ФИО владельца, Source, CreatedAt в публичный ответ не включаются (README:
владелец наружу не раскрывается; подпись «подборка от…» — зона фронта владельца/news).
PublicSelectionItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | SelectionItem.PropertyObjectId | — | Ключ для батча display-данных в catalog. |
SortOrder | int | Да | SelectionItem.SortOrder | — | Позиция 0..N-1. |
Comment | string? | Нет | SelectionItem.Comment | — | Комментарий владельца — публичен by design (фронт предупреждает владельца при вводе). |
Алгоритм
- Контекст и доступ. Операцию выполняет любой актор — аноним или авторизованный пользователь
(токен нужен только для вычисления
IsOwner). - Проверка входа и предусловий. Если feature flag
selections.public-open.enabledвыключен →SELECTIONS_NOT_FOUND(без раскрытия причины).PublicCode— ровно 12 символов[0-9A-Za-z]; нарушение формата отвечает тем жеSELECTIONS_NOT_FOUND(единый 404-контур против перебора). - Что читаем. Подборка по
PublicCode, не удалённая (DeletedAt == null), с элементами — горячий путь по partial-indexix_selections_public_code_active(SLO p99 < 150 мс). Нет кода или soft-deleted (INV-SEL-009) →SELECTIONS_NOT_FOUND. - Бизнес-правила и маппинг.
IsOwner= токен принадлежит владельцу подборки. Состав — элементы в порядкеSortOrder; внутреннийIdвключается только владельцу. Существование объектов недвижимости не проверяется — недоступные пометит батчcatalogна фронте (INV-SEL-008). - Результат. Модель
PublicSelectionQueryResult→200. Masking:OwnerUserIdисключён из модели;Id— conditional (шаг 4). Freshness: основная БД, read-your-writes для владельца.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
SELECTIONS_NOT_FOUND | 404 | Код не существует / формат неверен / подборка soft-deleted (INV-SEL-009) / флаг public-open выключен | «Подборка не найдена» | no retry |
TOO_MANY_REQUESTS | 429 | Превышен 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}, Then200сTitle/Description/Items(в порядкеSortOrder),IsOwner = false, полеIdотсутствует;OwnerUserIdне раскрыт. - Given тот же код, When открывает владелец (JWT его), Then
200,IsOwner = true, присутствует внутреннийIdдля кнопок редактирования. - Given код существует, но подборка soft-deleted (INV-SEL-009), When
GET, Then404 SELECTIONS_NOT_FOUND(то же, что для несуществующего кода — существование не раскрывается). - Given
publicCodeневерного формата (не 12 base62), WhenGET, Then404 SELECTIONS_NOT_FOUNDбез обращения к БД (единый 404-контур против перебора). - Given feature flag
selections.public-open.enabled = false, WhenGET(в т.ч. владельцем), Then404 SELECTIONS_NOT_FOUND; владелец пользуетсяGET /api/v1/selections. - Given поток 404-ответов с одного IP превысил порог (сканирование кодов), When очередной
GET, Then429 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: spanGetSelectionByPublicCodeQuery. Dashboard —selections-overview; runbook — аварийное отключение флагаselections.public-open.enabled.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): API контекста selections — индекс, GET /api/v1/internal/selections/{id} — Подборка для карточки клиента/сделки (m2m crm), GET /api/v1/selections — Мои подборки, POST /api/v1/selections — Создать подборку вручную, PUT /api/v1/selections/{id}/items — Заменить состав и порядок подборки целиком
- События (1): Event Contract: pin.selections.selection-shared (SelectionSharedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/selection.md (INV-SEL-005, 008, 009).
- Гейт/реф-ссылки: будущие
docs/06-services/referral/README.md,docs/06-services/catalog/README.md. - Соседние: list-my-selections.md, update-selection.md.