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

DELETE /api/investor/crm/leads/{leadId}/booking-intents/{intentId} — Инвестор снимает объект-намерение

Инвестор убирает объект из своих намерений в рамках лида (передумал по конкретному объекту). Снятие — мягкое (Status = Cancelled, запись сохраняется для истории); повторное намерение по тому же объекту можно завести заново. Дом полей — lead.md.

Назначение

Принять от инвестора идентификатор его активного намерения и перевести его в Status = Cancelled с проставлением RemovedAt = now, RemovedByUserId = actor.UserId. Если по намерению уже создана сделка (DealId != null), снятие запрещено — сначала отменяется сделка. Повтор по уже снятому намерению — идемпотентный no-op.

Ограничение

Если по намерению уже создана неотменённая сделка (DealId != null) — снятие запрещено: 409 CRM_BOOKING_INTENT_HAS_DEAL. Сначала отмените сделку по объекту.

Идемпотентность

Повторный DELETE по уже снятому намерению (Status = Cancelled) — no-op: возвращается 200 с текущей карточкой, новое событие ленты лида не добавляется.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areainvestor (JWT инвестора)
Feature groupbooking-intents
Статусdraft
Документdocs/06-services/crm/api/investor/remove-booking-intent.md

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

ПроверкаЗначение
Actor typeUser (Инвестор)
Auth policy / Permissionscrm.leads.participate (инвесторская сторона лида)
Scope (RBAC)не применяется: инвестор — вне agency-tenant (кросс-tenant участник, ADR-0052)
Record-levellead.PrimaryInvestorUserId = actor.UserId; намерение принадлежит этому лиду
Tenant isolationнамерение TenantId = lead.TenantId = AgencyCompanyId; инвестор кросс-tenant → изоляция record-level по лиду
Auditupdate/soft-delete (Status = Cancelled, RemovedAt, RemovedByUserId)
Доступ

Только сторона лида: lead.PrimaryInvestorUserId = actor.UserId. Инвестор — кросс-tenant участник (ADR-0052); изоляция — record-level по лиду, а не по scope/tenant.

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/investor/crm/leads/{leadId}/booking-intents/{intentId}
Success status200 (снято или идемпотентный повтор)
Idempotency headerне применяется (DELETE естественно идемпотентен: повтор по снятому — no-op)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
LeadIdrouteGuidДа!= Guid.Empty; существующий лид, инвестор — его сторона
IntentIdrouteGuidДа!= Guid.Empty; намерение лида
ActorUserIdidentity claimsGuidДаJWT инвестора; record-level = lead.PrimaryInvestorUserIdscope isolation

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

Та же карточка, что у add-booking-intent.md; значимые поля после операции — Status = Cancelled, RemovedAt заполнен.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (permission crm.leads.participate); ActorUserId — из токена; LeadId/IntentId — из route.
  2. Проверка входа и предусловий.
    • формат: LeadId/IntentId != Guid.Empty → иначе VALIDATION_FAILED;
    • лид существует → иначе CRM_LEAD_NOT_FOUND;
    • инвестор — сторона лида → иначе CRM_LEAD_FORBIDDEN;
    • намерение существует и принадлежит лиду → иначе CRM_BOOKING_INTENT_NOT_FOUND;
    • связанная сделка: если DealId != null и сделка не отменена → CRM_BOOKING_INTENT_HAS_DEAL (снять намерение можно только отменив сделку).
  3. Что читаем. Намерение по (LeadId, IntentId) — для проверки состояния и наличия сделки.
  4. Бизнес-правила и переходы. Если намерение уже Status = Cancelled — идемпотентный no-op: вернуть текущую карточку без записи. Иначе перевод Active → Cancelled: RemovedAt = now, RemovedByUserId = ActorUserId; в ленту лида добавить LeadEventType.BookingIntentRemoved (payload LeadBookingIntentRefPayload { IntentId, PropertyObjectId, DeveloperCompanyId }) и обновить LastEventAt.
  5. Что меняется. Обновляются статус/атрибуты снятия намерения и событие ленты лида — в одном согласованном изменении.
  6. Результат. Карточка намерения (Status = Cancelled) → 200.

Побочные эффекты

ТипДетали
Интеграционные событияnone
Внешние вызовыnone
Проекции / поискnone
Уведомленияопосредованно: агент видит снятие в ленте лида (LastEventAt)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400LeadId/IntentId == Guid.Empty«Проверьте корректность запроса»no retry
UNAUTHORIZED401нет/невалидный JWT«Требуется авторизация»no retry
CRM_LEAD_NOT_FOUND404лид не найден«Лид не найден»no retry
CRM_LEAD_FORBIDDEN403инвестор не сторона лида«Нет доступа к лиду»no retry
CRM_BOOKING_INTENT_NOT_FOUND404намерение не найдено / другого лида«Намерение не найдено»no retry
CRM_BOOKING_INTENT_HAS_DEAL409по намерению есть неотменённая сделка (DealId != null)«Сначала отмените сделку по объекту»no retry

Acceptance Criteria

  • AC-1 (happy — снятие). Дано: активное намерение без сделки (DealId = null). Когда: DELETE. Тогда: 200, Status = Cancelled, RemovedAt/RemovedByUserId заполнены, в ленте лида BookingIntentRemoved.
  • AC-2 (идемпотентный повтор). Дано: намерение уже Cancelled. Когда: повтор DELETE. Тогда: 200 (no-op), новое событие не добавляется.
  • AC-3 (есть сделка). Дано: DealId != null, сделка не отменена. Когда: DELETE. Тогда: 409 CRM_BOOKING_INTENT_HAS_DEAL.
  • AC-4 (чужой лид). Дано: lead.PrimaryInvestorUserId != actor. Когда: DELETE. Тогда: 403 CRM_LEAD_FORBIDDEN.

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

  • Route стабилен; поля ответа additive; сужение enum намерения — breaking (migration + rollout).
  • Logs: booking-intent removed (IntentId, LeadId); metrics: crm_booking_intents_removed_total{result}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm.

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

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

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