POST /api/investor/crm/leads/{leadId}/booking-intents/{intentId}/confirm — Инвестор подтверждает предложенное намерение
Инвестор подтверждает объект, который агент предложил ему в рамках лида (Flow B): агент завёл намерение
Source = AgentProposed (или создал сделку по незапрошенному объекту в статусе Draft), а инвестор
соглашается работать по этому объекту. Подтверждение — обязательное условие гейта Draft → Application
сделки. Дом полей — lead.md.
Назначение
Принять от инвестора идентификатор предложенного агентом намерения и проставить ConfirmedByInvestor = true,
ConfirmedAt = now. Операция не создаёт сделку и не вовлекает застройщика — она снимает одно из двух условий
перевода сделки из Draft в Application (второе — одобренная фиксация для пары инвестор × застройщик).
Подтверждение уже подтверждённого намерения — идемпотентный no-op.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | investor (JWT инвестора) |
| Feature group | booking-intents |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/confirm-booking-intent.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Инвестор) |
| Auth policy / Permissions | crm.leads.participate (инвесторская сторона лида) |
| Scope (RBAC) | не применяется: инвестор — вне agency-tenant (кросс-tenant участник, ADR-0052) |
| Record-level | lead.PrimaryInvestorUserId = actor.UserId; намерение принадлежит этому лиду |
| Tenant isolation | намерение TenantId = lead.TenantId = AgencyCompanyId; инвестор кросс-tenant → изоляция record-level по лиду |
| Audit | update (ConfirmedByInvestor, ConfirmedAt) |
Выполнить операцию может только инвестор — основная сторона лида (lead.PrimaryInvestorUserId = actor.UserId). Инвестор — кросс-tenant участник (вне agency-tenant, ADR-0052): доступ проверяется
record-level по лиду, а не по scope.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investor/crm/leads/{leadId}/booking-intents/{intentId}/confirm |
| Success status | 200 (подтверждено или идемпотентный повтор) |
| Idempotency header | не применяется (повтор при ConfirmedByInvestor = true — no-op) |
| Correlation | сквозной trace через заголовок traceparent |
Отдельного idempotency-ключа нет: повторное подтверждение уже подтверждённого намерения
(ConfirmedByInvestor = true) — идемпотентный no-op, возвращается текущая карточка без изменений.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
LeadId | route | Guid | Да | != Guid.Empty; существующий лид Status = Active, инвестор — его сторона | — |
IntentId | route | Guid | Да | != Guid.Empty; существующее активное намерение лида | — |
ActorUserId | identity claims | Guid | Да | JWT инвестора; record-level = lead.PrimaryInvestorUserId | scope isolation |
Модель ответа BookingIntentCardModel
Та же карточка, что у add-booking-intent.md (дом полей —
lead.md); значимое поле операции — ConfirmedByInvestor
= true, ConfirmedAt заполнен.
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (permission
crm.leads.participate);ActorUserId— из токена;LeadId/IntentId— из route. - Проверка входа и предусловий.
- формат:
LeadId/IntentId!= Guid.Empty→ иначеVALIDATION_FAILED; - лид существует → иначе
CRM_LEAD_NOT_FOUND; - инвестор — сторона лида → иначе
CRM_LEAD_FORBIDDEN; - лид активен (
Status = Active) → иначеCRM_LEAD_NOT_ACTIVE; - намерение существует, принадлежит лиду и
Status = Active→ иначеCRM_BOOKING_INTENT_NOT_FOUND(снятое/чужое намерение недостижимо).
- формат:
- Что читаем. Намерение по (
LeadId,IntentId) — для проверки состояния и подтверждения. - Бизнес-правила и переходы. Если
ConfirmedByInvestor = trueуже — идемпотентный no-op: вернуть текущую карточку без записи. Иначе проставитьConfirmedByInvestor = true,ConfirmedAt = now; в ленту лида добавитьLeadEventType.BookingIntentConfirmed(payloadLeadBookingIntentRefPayload { IntentId, PropertyObjectId, DeveloperCompanyId }) и обновитьLastEventAt. Подтверждение само по себе сделку не переводит: переводDraft → Applicationинициирует агент отдельной операцией при выполнении обоих условий (подтверждённое намерение + одобренная фиксация). - Что меняется. Обновляются флаг/дата подтверждения на намерении и событие ленты лида — в одном согласованном изменении.
- Результат. Карточка намерения (
ConfirmedByInvestor = true) →200.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (застройщик не вовлекается на подтверждении; developer-facing события — только при Draft → Application) |
| Внешние вызовы | none |
| Проекции / поиск | none |
| Уведомления | опосредованно: агент видит подтверждение в ленте лида (LastEventAt) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | LeadId/IntentId == Guid.Empty | «Проверьте корректность запроса» | no retry |
UNAUTHORIZED | 401 | нет/невалидный JWT | «Требуется авторизация» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден | «Лид не найден» | no retry |
CRM_LEAD_FORBIDDEN | 403 | инвестор не сторона лида | «Нет доступа к лиду» | no retry |
CRM_LEAD_NOT_ACTIVE | 409 | lead.Status != Active | «Лид неактивен» | no retry |
CRM_BOOKING_INTENT_NOT_FOUND | 404 | намерение не найдено / снято / другого лида | «Намерение не найдено» | no retry |
Снятое (Status = Cancelled) или намерение другого лида возвращает 404 CRM_BOOKING_INTENT_NOT_FOUND, а не
403 — операция не раскрывает существование чужих намерений.
Acceptance Criteria
- AC-1 (happy — подтверждение). Дано: активное
AgentProposed-намерение (ConfirmedByInvestor = false). Когда:POST …/confirm. Тогда:200,ConfirmedByInvestor = true,ConfirmedAtзаполнен, в ленте лидаBookingIntentConfirmed. - AC-2 (идемпотентный повтор). Дано: намерение уже
ConfirmedByInvestor = true. Когда: повтор. Тогда:200(no-op), новое событие не добавляется. - AC-3 (снятое намерение). Дано: намерение
Status = Cancelled. Когда:POST. Тогда:404 CRM_BOOKING_INTENT_NOT_FOUND. - AC-4 (чужой лид). Дано:
lead.PrimaryInvestorUserId != actor. Когда:POST. Тогда:403 CRM_LEAD_FORBIDDEN.
Совместимость и наблюдаемость
- Route стабилен; поля ответа additive; сужение enum намерения — breaking (migration + rollout).
- Logs:
booking-intent confirmed(IntentId,LeadId); metrics:crm_booking_intents_confirmed_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): DELETE /api/investor/crm/leads/{leadId}/booking-intents/{intentId} — Инвестор снимает объект-намерение, GET /api/agent/crm/leads/{leadId}/booking-intents — Список намерений лида, POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application), POST /api/agent/crm/leads/{leadId}/booking-intents — Агент предлагает объект клиенту, POST /api/investor/crm/leads/{leadId}/booking-intents — Инвестор добавляет объект-намерение
- Use Cases (1): UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, INV-INTENT-1, enum-ы): lead.md;
лид и
LeadEventType— lead.md. - Смежные методы: add-booking-intent.md, remove-booking-intent.md, propose-booking-intent.md.
- Гейт сделки
Draft → Application: create-deal.md (Flow B); карта области — README. ADR: 0071 company-ownership и участники.