POST /api/agent/crm/deals/{dealId}/selection — Отправить подборку в сделку
Агент отправляет клиенту подборку объектов в рамках открытой сделки — карточка подборки попадает в чат
сделки, а факт фиксируется во вкладке «Ход сделки». Операция — точечная запись в таймлайн сделки
(deal-timeline-event.md, тип SelectionSent); дом полей сделки —
deal.md.
Назначение
Принять сделку и подборку, проверить существование подборки в selections и нетерминальность сделки, затем
материализовать в таймлайн событие SelectionSent с типизированным snapshot TimelineSelectionPayload { SelectionId, SelectionTitle }. Собственного Kafka-события операция не публикует: доставку карточки в чат
сделки выполняет messenger (владелец чатов, ADR-0055), а таймлайн — локальная материализация факта.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent (JWT агента) |
| Feature group | deal |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/send-deal-selection.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент — ответственный по сделке); Operator (Менеджер ПИН — platform-scope) |
| Auth policy / Permissions | crm.deals.manage |
| Scope (RBAC) | организация (агентство): record-level по сделке |
| Record-level | актор — активный участник-исполнитель (Executor) сделки либо активный участник PinManager / Менеджер ПИН |
| Tenant isolation | deal.TenantId = DeveloperCompanyId (ADR-0052); актор-агент — в tenant своего агентства, поэтому сделка читается record-level по членству участника (актор — Executor, санкционировано доменной моделью) |
| Audit | append-only факт таймлайна (ActorUserId = actor.UserId); PII в payload отсутствует |
Отправить подборку может только активный участник-исполнитель (Executor) сделки либо активный участник
PinManager / Менеджер ПИН (platform-scope, обходит record-level). В отличие от некоторых операций CRM, факт
существования чужой сделки не скрывается: сделка другого агента/агентства отвечает 403 CRM_DEAL_FORBIDDEN, а не
404.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/deals/{dealId}/selection |
| Success status | 204 (подборка отправлена; тела нет) |
| Idempotency header | не применяется (повторная отправка подборки — легитимное действие, каждый вызов = новая запись таймлайна) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route | Guid | Да | != Guid.Empty; существующая нетерминальная сделка, доступная актору | — |
SelectionId | body | Guid | Да | != Guid.Empty; существующая подборка в selections (internal-lookup) | — |
ActorUserId | identity claims | Guid | Да | JWT агента; record-level по членству участника-исполнителя (Executor, кроме Менеджера ПИН) | — |
ActorAgencyCompanyId | identity claims | Guid | Да | tenant агентства актора (record-level) | Tenant isolation |
Модель ответа
Тела ответа нет: HTTP 204 No Content. Идентификатор записи таймлайна наружу не возвращается (лента читается
отдельной query вкладки «Ход сделки»). При ошибке — ProblemDetails с кодом.
Алгоритм
- Контекст и доступ. Операцию выполняет агент — ответственный по сделке (permission
crm.deals.manage) либо Менеджер ПИН. - Проверка входа и предусловий.
- формат:
DealId(route) иSelectionId(body)!= Guid.Empty— иначе ошибка валидации; - сделка существует — иначе
CRM_DEAL_NOT_FOUND; - record-level: актор — активный участник-исполнитель (
deal.ExecutorAgentUserId == actor, рольExecutor) либо активный участникPinManager/ Менеджер ПИН — иначеCRM_DEAL_FORBIDDEN; - допустимость состояния (INV-DEAL-9):
deal.Status ∈ {Application, Booking, Sale}(нетерминальная);Completed/Cancelled→CRM_DEAL_ALREADY_TERMINAL; - подборка существует: internal-вызов
selectionsGET /internal/selections/{SelectionId}→404→CRM_SELECTION_NOT_FOUND; из ответа берётсяSelectionTitle(snapshot для payload); недоступностьselections→CRM_SELECTIONS_UNAVAILABLE(retry клиентом).
- формат:
- Что читаем. Сделку (родитель — цель материализации таймлайна). Дочерние записи таймлайна не подгружаются (append-only, дублей по построению нет).
- Бизнес-правила и переходы. Мутации статуса/полей сделки нет — только append в таймлайн. Создать запись
DealTimelineEventEntity(guard INV-DEAL-9: нетерминальная):Type = SelectionSent,Status = Done(факт без целевого действия — deal-timeline-event.md),Title = "Отправлена подборка",OccurredAt = now,ActorUserId = actor,ActorRole = Agent,Payload = TimelineSelectionPayload { SelectionId, SelectionTitle }(ContractVersion = 1). - Что меняется. Появляется дочерняя запись таймлайна сделки; контент сделки не меняется (обновляется только
UpdatedAt). - События. Интеграционные Kafka-события не публикуются: у операции нет собственного crm-события —
таймлайн Kafka не публикует (deal-timeline-event.md); downstream-топика
на «событие таймлайна» в README нет. Доставка карточки подборки в чат сделки —
ответственность
messenger(владелец чатов, ADR-0055; чат провижинится по deal-created.md); эта операция в чат не пишет. - Результат. HTTP
204 No Content.
Диаграмма
Предусловия и постусловия
Предусловия. Актор — ответственный агент по сделке (активный участник-исполнитель, deal.ExecutorAgentUserId = actor) либо активный участник
PinManager / Менеджер ПИН, с crm.deals.manage. Сделка существует и нетерминальна (Status ∈ {Application, Booking, Sale}, INV-DEAL-9). Подборка существует в контексте selections.
Существование подборки проверяется синхронным internal-lookup (timeout 2 c, retry 1); из ответа берётся
SelectionTitle — иммутабельный snapshot для payload. При недоступности источника операция завершается отказом
(CRM_SELECTIONS_UNAVAILABLE), запись таймлайна не создаётся.
Постусловие (успех). В таймлайн добавлена append-only запись SelectionSent (Status = Done,
TimelineSelectionPayload { SelectionId, SelectionTitle }, ContractVersion = 1); Deal.UpdatedAt обновлён,
статус и контент сделки не изменены. Собственного Kafka-события crm нет. Идемпотентности нет намеренно:
повторная отправка той же подборки — легитимна, каждый вызов создаёт новую запись.
Запись SelectionSent видна во вкладке «Ход сделки» сразу после 204, а карточка подборки доставляется в чат
сделки асинхронно через messenger (владелец чатов, ADR-0055) и появляется с лагом.
Постусловие (отказ). Запись таймлайна не создаётся, Deal.UpdatedAt не меняется.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none — собственного crm kafka-события нет (таймлайн Kafka не публикует, deal-timeline-event.md); доменное TimelineItemAdded (in-memory) |
| Внешние вызовы | selections internal-lookup GET /internal/selections/{id} — существование подборки + SelectionTitle (timeout 2 c, retry 1) |
| Проекции / поиск | нет (таймлайн — append-only; проекция сделки DealSearchDocument не затрагивается) |
| Уведомления | прямого push от CRM нет (нет kafka-события); участников уведомляет карточка подборки в чате сделки (messenger, ADR-0055), запись SelectionSent видна во вкладке «Ход сделки» |
Ошибки и failure modes
Терминальность сделки (Status ∈ {Completed, Cancelled}, INV-DEAL-9) необратима — отправка подборки в
завершённую/отменённую сделку всегда 409 CRM_DEAL_ALREADY_TERMINAL. Недоступность selections — единственная
retryable ошибка (503 CRM_SELECTIONS_UNAVAILABLE).
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId/SelectionId = Guid.Empty | «Проверьте корректность запроса» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделка не найдена | «Сделка не найдена» | no retry |
CRM_DEAL_FORBIDDEN | 403 | сделка другого агента/агентства | «Нет доступа к сделке» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | deal.Status ∈ {Completed, Cancelled} (INV-DEAL-9) | «Сделка завершена или отменена» | no retry |
CRM_SELECTION_NOT_FOUND | 404 | подборка не найдена в selections | «Подборка не найдена» | no retry |
CRM_SELECTIONS_UNAVAILABLE | 503 | selections недоступен | «Сервис подборок временно недоступен» | retry |
Совместимость и наблюдаемость
TimelineSelectionPayload— versioned jsonb (ContractVersion = 1), совместимость additive-only; breaking → новыйContractVersion(дом типов — deal-timeline-event.md). Тело запроса{SelectionId}— минимальное; новые optional-поля без breaking change.- Logs:
deal-selection sent(DealId,SelectionId,ActorUserId; PII нет); metrics:crm_deal_selection_sent_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given нетерминальная сделка ответственного агента и существующая подборка; When
POST; Then в таймлайн добавлена записьSelectionSent(Done) со snapshotSelectionTitle,204, Kafka-событий нет. - Подборка не найдена. Given
selectionsвернул404; WhenPOST; Then404 CRM_SELECTION_NOT_FOUND, таймлайн не изменён. - Selections недоступен. Given timeout после retry; When
POST; Then503 CRM_SELECTIONS_UNAVAILABLE, таймлайн не изменён; клиент повторяет. - Терминальная сделка. Given
Status ∈ {Completed, Cancelled}; WhenPOST; Then409 CRM_DEAL_ALREADY_TERMINAL(INV-DEAL-9). - Чужая сделка. Given сделка другого агента/агентства, где актор не участник; When
POST; Then403 CRM_DEAL_FORBIDDEN. - Сделка не найдена. Given несуществующая сделка; When
POST; Then404 CRM_DEAL_NOT_FOUND. - Валидация. Given
DealIdилиSelectionId=Guid.Empty; WhenPOST; Then400 VALIDATION_FAILED. - Повторная отправка. Given одна и та же подборка; When
POSTвызван дважды; Then две записи таймлайна (по одной на вызов — операция не идемпотентна по замыслу). - Downstream (eventual). Given успешная отправка; When
messengerобработал факт; Then карточка подборки появляется в чате сделки, а записьSelectionSentуже видна во вкладке «Ход сделки».
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События контекста selections: карта топиков и отклонённые события, Event Contract: pin.selections.selection-shared (SelectionSharedKafkaEvent)
- registries (1): Реестр API-методов PIN
- index.md (1): Selections Service (Pin.Selections)
Связанные документы
- Сущности: deal.md (агрегат, статусы, INV-DEAL-9),
deal-timeline-event.md (тип
SelectionSent,TimelineSelectionPayload,TimelineItemStatus,TimelineItemAdded). - Чат сделки: deal-created.md (провижининг чата
messenger); карта области — README; консюмер ленты лида для подборок — selection-shared.md. - ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first.