GET /api/v1/internal/selections/{id} — Подборка для карточки клиента/сделки (m2m crm)
Синхронное internal-чтение подборки по сохранённому у crm SelectionId для карточки клиента
(лид/сделка, страница /crm/clients/[id]/collections). В отличие от публичного открытия по коду,
ответ адресован доверенному сервису: включает владельца (OwnerUserId) и способ создания
(Source), а soft-deleted подборку не прячет под 404 — резолвит её и сообщает статус удаления,
чтобы CRM показал «подборка недоступна/удалена» (INV-SEL-009 действует только на публичное чтение).
Назначение
crm хранит у себя SelectionId подборки, которую агент отправил клиенту в лиде/сделке. Открывая
карточку клиента, CRM вызывает этот метод и получает подборку владельца-агента для отображения блока
«подборка от агента X»: название, описание, состав, публичную ссылку и — ключевое — признак удаления.
Право агента на подборку в лиде обеспечивает сам crm (доверенный вызыватель по сохранённому id),
поэтому record-level фильтр по OwnerUserId здесь не применяется. Display-данные объектов недвижимости
CRM/фронт добирает батчем в catalog — метод отдаёт только состав. Дом полей —
../domain/selection.md.
| Поле | Значение |
|---|
| Сервис/контекст | selections |
| API area | internal |
| Feature group | selections |
| Статус | draft |
| Документ | docs/06-services/selections/api/internal/get-selection.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — m2m-клиент crm (client-credentials) |
| Auth policy / Permissions | authorization policy InternalCrmOnly (m2m-клиент = crm); пользовательского пути к операции нет |
| Scope (RBAC) | не применяется; право агента на подборку в лиде проверяет crm до вызова (доверенный сервис читает по сохранённому у себя SelectionId) |
| Record-level | не применяется — фильтр по OwnerUserId снят намеренно: crm читает чужую (владельца-агента) подборку по доверенному id |
| Tenant isolation | не применяется — вне tenant (ADR-0052 п.2: подборка личная, владелец — инвестор или агент) |
| Audit | read; PII нет; OwnerUserId/IsDeleted в логах операции |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/internal/selections/{id:guid} |
| Success status | 200 (в т.ч. для soft-deleted подборки — с IsDeleted = true) |
| Idempotency header | не применяется (безопасный read) |
| Correlation | сквозной trace через заголовок traceparent, из crm |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
SelectionId | route ({id}) | Guid | Да | != Guid.Empty | доверенный m2m; record-level фильтр не применяется |
Body отсутствует.
Модель ответа InternalSelectionResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
SelectionId | Guid | Да | Selection.Id | — | Echo сохранённого у crm id. |
OwnerUserId | Guid | Да | Selection.OwnerUserId | отдаётся (trusted) | Владелец-агент — для подписи «подборка от агента X» в карточке клиента. |
Title | string | Да | Selection.Title | — | Название. |
Description | string? | Нет | Selection.Description | — | Описание. |
PublicCode | string | Да | Selection.PublicCode | — | Публичный код для ссылки /collection/{publicCode} из карточки клиента. |
Source | SelectionSource (enum строкой) | Да | Selection.Source | — | Manual / Favorites — как создана (аудит для CRM). |
IsDeleted | bool | Да | computed: Selection.DeletedAt != null | — | true → CRM показывает «подборка недоступна/удалена». |
DeletedAt | DateTimeOffset? | Нет | Selection.DeletedAt | — | Момент soft delete (только при IsDeleted == true). |
CreatedAt | DateTimeOffset | Да | Selection.CreatedAt | — | Создание. |
UpdatedAt | DateTimeOffset | Да | Selection.UpdatedAt | — | Последняя правка. |
Items | IReadOnlyList<InternalSelectionItemModel> | Да (может быть пуст) | Selection.Items order by SortOrder | — | Состав в порядке 0..N-1. |
InternalSelectionItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
PropertyObjectId | Guid | Да | SelectionItem.PropertyObjectId | — | Ключ для батча display-данных в catalog (недоступный объект недвижимости пометит CRM/фронт). |
SortOrder | int | Да | SelectionItem.SortOrder | — | Позиция 0..N-1 (INV-SEL-006). |
Comment | string? | Нет | SelectionItem.Comment | — | Комментарий владельца к объекту недвижимости. |
Алгоритм
- Контекст и доступ. Доверенный m2m
crm (client-credentials); вызыватель не crm →
403 FORBIDDEN. Record-level фильтр по OwnerUserId не накладывается — crm читает чужую
(владельца-агента) подборку по доверенному id.
- Проверка входа и предусловий.
SelectionId непустой, иначе VALIDATION_ERROR.
- Что читаем. Подборка по id с элементами — без условия
DeletedAt == null (в отличие от
публичного чтения: CRM обязан различать «удалена» и «нет записи»). Строки нет вообще →
SELECTIONS_NOT_FOUND.
- Бизнес-правила и маппинг. Маппинг поле-в-поле:
OwnerUserId, Source, PublicCode отдаются
(адресат доверенный); IsDeleted = подборка soft-deleted; DeletedAt — момент удаления; Items —
элементы в порядке SortOrder (PropertyObjectId, SortOrder, Comment). Существование объектов
недвижимости не проверяется (INV-SEL-008) — недоступные пометит батч catalog на стороне CRM/фронта.
- Результат. Модель
InternalSelectionResult → 200 — в том числе для soft-deleted подборки
(с IsDeleted = true); показать «недоступна/удалена» решает сам CRM.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_ERROR | 400 | SelectionId == Guid.Empty | «Некорректный идентификатор» | no retry |
SELECTIONS_NOT_FOUND | 404 | Строки нет вообще (никогда не существовала) — soft-deleted не сюда, она резолвится с IsDeleted = true | «Подборка не найдена» | no retry |
FORBIDDEN | 403 | Вызыватель не m2m crm (policy InternalCrmOnly) | — | no retry |
Кросс-сервисный контракт (selections ↔ crm)
selections ожидает от crm: валидный m2m client-credentials токен (policy InternalCrmOnly);
SelectionId, ранее полученный crm из события selection-shared (key = SelectionId) или из
отправки в сделку. crm — доверенный вызыватель: право агента на подборку в лиде проверено на стороне
crm до вызова, поэтому record-level по OwnerUserId здесь снят.
selections предоставляет crm: подборку владельца-агента с OwnerUserId/Source/PublicCode
(адресат доверенный) и признаком удаления IsDeleted/DeletedAt. Soft-deleted подборка резолвится
(не прячется под 404) — crm обязан различать «удалена» и «нет записи». Состав отдаётся без
display-данных объектов недвижимости — их CRM/фронт добирает батчем в catalog.
- Поведение при устаревании:
SelectionId у crm вечно разрешим (soft delete + FK restrict от
snapshot-ов не дают строке исчезнуть); crm не кэширует состав — при каждом открытии карточки читает
актуальную версию, поэтому переименование/правка состава видны сразу.
Предусловия и постусловия
- Предусловия: вызыватель — доверенный m2m
crm; SelectionId непустой.
- Постусловия: состояние не меняется (безопасное чтение); при существующей строке возвращается
подборка (в т.ч. soft-deleted — с
IsDeleted = true); OwnerUserId отдаётся (trusted-адресат);
Items в порядке SortOrder 0..N-1.
Acceptance Criteria
- Given активная подборка, When
crm вызывает GET /internal/selections/{id}, Then 200 с
OwnerUserId, Source, PublicCode, IsDeleted = false и составом в порядке SortOrder.
- Given soft-deleted подборка, When
crm читает по сохранённому id, Then 200 с IsDeleted = true и
заполненным DeletedAt — CRM показывает «подборка недоступна/удалена» (не 404).
- Given
SelectionId, которого никогда не было, When GET, Then 404 SELECTIONS_NOT_FOUND (в отличие
от soft-deleted).
- Given вызыватель не
crm (другой m2m или пользовательский токен), When GET, Then 403 FORBIDDEN
(policy InternalCrmOnly).
- Given
SelectionId == Guid.Empty, When GET, Then 400 VALIDATION_ERROR.
- Given подборка с недоступным объектом недвижимости в составе, When
GET, Then элемент отдаётся как
есть (существование не проверяется, INV-SEL-008) — недоступность пометит батч catalog на стороне CRM.
- Given сетевой сбой/5xx, When
crm повторяет GET, Then повтор безопасен (read-only, без побочных
эффектов).
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; удаление/переименование/смена типа — breaking (migration +
rollout, согласование с
crm). Source/IsDeleted — семантика для доверенного адресата, в
публичный ответ (get-selection-by-code.md) не просачиваются.
- Logs:
SelectionId, IsDeleted (без PII); metrics: selections_internal_read_total{is_deleted};
traces: сквозной span из crm. Dashboard — selections-overview;
alert — рост 5xx internal-области (деградация карточек клиента в CRM, ../README.md).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы