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

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 areaagent (JWT агента)
Feature groupdeal
Статусdraft
Документdocs/06-services/crm/api/agent/send-deal-selection.md

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

ПроверкаЗначение
Actor typeUser (Агент — ответственный по сделке); Operator (Менеджер ПИН — platform-scope)
Auth policy / Permissionscrm.deals.manage
Scope (RBAC)организация (агентство): record-level по сделке
Record-levelактор — активный участник-исполнитель (Executor) сделки либо активный участник PinManager / Менеджер ПИН
Tenant isolationdeal.TenantId = DeveloperCompanyId (ADR-0052); актор-агент — в tenant своего агентства, поэтому сделка читается record-level по членству участника (актор — Executor, санкционировано доменной моделью)
Auditappend-only факт таймлайна (ActorUserId = actor.UserId); PII в payload отсутствует
Доступ

Отправить подборку может только активный участник-исполнитель (Executor) сделки либо активный участник PinManager / Менеджер ПИН (platform-scope, обходит record-level). В отличие от некоторых операций CRM, факт существования чужой сделки не скрывается: сделка другого агента/агентства отвечает 403 CRM_DEAL_FORBIDDEN, а не 404.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/agent/crm/deals/{dealId}/selection
Success status204 (подборка отправлена; тела нет)
Idempotency headerне применяется (повторная отправка подборки — легитимное действие, каждый вызов = новая запись таймлайна)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty; существующая нетерминальная сделка, доступная актору
SelectionIdbodyGuidДа!= Guid.Empty; существующая подборка в selections (internal-lookup)
ActorUserIdidentity claimsGuidДаJWT агента; record-level по членству участника-исполнителя (Executor, кроме Менеджера ПИН)
ActorAgencyCompanyIdidentity claimsGuidДаtenant агентства актора (record-level)Tenant isolation

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

Тела ответа нет: HTTP 204 No Content. Идентификатор записи таймлайна наружу не возвращается (лента читается отдельной query вкладки «Ход сделки»). При ошибке — ProblemDetails с кодом.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент — ответственный по сделке (permission crm.deals.manage) либо Менеджер ПИН.
  2. Проверка входа и предусловий.
    • формат: 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/CancelledCRM_DEAL_ALREADY_TERMINAL;
    • подборка существует: internal-вызов selections GET /internal/selections/{SelectionId}404CRM_SELECTION_NOT_FOUND; из ответа берётся SelectionTitle (snapshot для payload); недоступность selectionsCRM_SELECTIONS_UNAVAILABLE (retry клиентом).
  3. Что читаем. Сделку (родитель — цель материализации таймлайна). Дочерние записи таймлайна не подгружаются (append-only, дублей по построению нет).
  4. Бизнес-правила и переходы. Мутации статуса/полей сделки нет — только 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).
  5. Что меняется. Появляется дочерняя запись таймлайна сделки; контент сделки не меняется (обновляется только UpdatedAt).
  6. События. Интеграционные Kafka-события не публикуются: у операции нет собственного crm-события — таймлайн Kafka не публикует (deal-timeline-event.md); downstream-топика на «событие таймлайна» в README нет. Доставка карточки подборки в чат сделки — ответственность messenger (владелец чатов, ADR-0055; чат провижинится по deal-created.md); эта операция в чат не пишет.
  7. Результат. HTTP 204 No Content.

Диаграмма

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

Предусловия. Актор — ответственный агент по сделке (активный участник-исполнитель, deal.ExecutorAgentUserId = actor) либо активный участник PinManager / Менеджер ПИН, с crm.deals.manage. Сделка существует и нетерминальна (Status ∈ {Application, Booking, Sale}, INV-DEAL-9). Подборка существует в контексте selections.

Ожидания от 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 нет. Идемпотентности нет намеренно: повторная отправка той же подборки — легитимна, каждый вызов создаёт новую запись.

Eventual consistency

Запись 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_FAILED400DealId/SelectionId = Guid.Empty«Проверьте корректность запроса»no retry
CRM_DEAL_NOT_FOUND404сделка не найдена«Сделка не найдена»no retry
CRM_DEAL_FORBIDDEN403сделка другого агента/агентства«Нет доступа к сделке»no retry
CRM_DEAL_ALREADY_TERMINAL409deal.Status ∈ {Completed, Cancelled} (INV-DEAL-9)«Сделка завершена или отменена»no retry
CRM_SELECTION_NOT_FOUND404подборка не найдена в selections«Подборка не найдена»no retry
CRM_SELECTIONS_UNAVAILABLE503selections недоступен«Сервис подборок временно недоступен»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; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/.

Acceptance Criteria

  • Happy path. Given нетерминальная сделка ответственного агента и существующая подборка; When POST; Then в таймлайн добавлена запись SelectionSent (Done) со snapshot SelectionTitle, 204, Kafka-событий нет.
  • Подборка не найдена. Given selections вернул 404; When POST; Then 404 CRM_SELECTION_NOT_FOUND, таймлайн не изменён.
  • Selections недоступен. Given timeout после retry; When POST; Then 503 CRM_SELECTIONS_UNAVAILABLE, таймлайн не изменён; клиент повторяет.
  • Терминальная сделка. Given Status ∈ {Completed, Cancelled}; When POST; Then 409 CRM_DEAL_ALREADY_TERMINAL (INV-DEAL-9).
  • Чужая сделка. Given сделка другого агента/агентства, где актор не участник; When POST; Then 403 CRM_DEAL_FORBIDDEN.
  • Сделка не найдена. Given несуществующая сделка; When POST; Then 404 CRM_DEAL_NOT_FOUND.
  • Валидация. Given DealId или SelectionId = Guid.Empty; When POST; Then 400 VALIDATION_FAILED.
  • Повторная отправка. Given одна и та же подборка; When POST вызван дважды; Then две записи таймлайна (по одной на вызов — операция не идемпотентна по замыслу).
  • Downstream (eventual). Given успешная отправка; When messenger обработал факт; Then карточка подборки появляется в чате сделки, а запись SelectionSent уже видна во вкладке «Ход сделки».

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

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

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