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

POST /api/investor/crm/deals/{dealId}/acknowledge-terms — Отметить «Ознакомлен»

Инвестор подтверждает, что ознакомился с условиями сделки — платное бронирование, ДДУ/ДКП, ипотека, рассрочка (US-INV-18). Операция фиксирует факт ознакомления на сделке DealEntity (поле TermsAcknowledgedAt) и добавляет строку таймлайна TermsAcknowledged («Ход сделки», US-INV-09). Сами условия инвестор читает в карточке сделки: условия бронирования — снимок BookingProgramTerms текущей брони (booking-program.md), условия способа оплаты (ДДУ/ДКП, ипотека, рассрочка с графиком) — из PaymentMethod/InstallmentPresetId сделки (purchase-program.md). Сохраняется только факт ознакомления; условия не перечитываются и не изменяются. Интеграционных событий нет.

Назначение

Дать авторизованному инвестору из карточки сделки отметить «Ознакомлен» с условиями — после того как он прочитал платность брони, тип договора и график оплаты (US-INV-18). Владелец — контекст crm, feature group deals, сценарий US-INV-18.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areainvestor
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/investor/acknowledge-terms.md

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

ПроверкаЗначение
Actor typeUser (Инвестор — участник сделки)
Auth policy / Permissionsauthenticated (JWT инвестора); отдельного RBAC-permission не требуется — self-service отметка
Scope (RBAC)не применяется: инвестор — вне developer-tenant (кросс-tenant участник, ADR-0052)
Record-levelDealEntity.PrimaryInvestorUserId = actor.UserId — форсится сервером; чужую сделку отметить нельзя
Tenant isolationне developer-tenanted: Deal.TenantId = DeveloperCompanyId ≠ актор; изоляция — record-level по PrimaryInvestorUserId из claims (ADR-0052)
Auditupdate (lifecycle): TermsAcknowledgedAt, UpdatedByUserId; ФИО/телефон/эскроу в логи не пишутся
Доступ

Отметить может только сам инвестор — участник сделки; PrimaryInvestorUserId форсится сервером из токена актора и не принимается из тела/маршрута. Чужая, несуществующая или удалённая сделка неотличимы снаружи — всегда CRM_DEAL_NOT_FOUND.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investor/crm/deals/{dealId}/acknowledge-terms
Success status200 (обновлённая карточка сделки)
Idempotency headerне применяется (бизнес-идемпотентность по TermsAcknowledgedAt)
Correlationсквозной trace через заголовок traceparent

Тело запроса пустое (маркер без полезной нагрузки); операция определяется по маршруту и токену инвестора.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty; существующая сделка актора-инвестора
PrimaryInvestorUserIdidentity claimsGuidДаRequired; record-level предикат, форсится = actor.UserId (из body/route не берётся)scope isolation

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

Возвращается обновлённая карточка сделки (проекция карточки /profile/deals/[dealId]). Дом модели — get-deal.md (DealCardModel), дом полей — сущность DealEntity. Ниже — только дельта, изменяемая этой операцией; остальные поля карточки — как в доме.

ПолеТипОбязательностьИсточникMaskingОписание
DealIdGuidДаDealEntity.IdИдентификатор сделки.
StatusDealStatusДаDealEntity.Status (enum строкой)Без изменений (отметка не меняет статус): Application/Booking/Sale.
TermsAcknowledgedAtDateTimeOffset?НетDealEntity.TermsAcknowledgedAtПроставленный момент отметки «Ознакомлен» (US-INV-18).
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtДата последнего обновления (US-INV-04).

Все enum — строкой; EscrowAccountNumber в остальных полях карточки наружу маскируется (****NNNN, дом — deal.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор — участник сделки; PrimaryInvestorUserId берётся из его токена (не из запроса). Бизнес-идемпотентность: если у сделки уже проставлена отметка (TermsAcknowledgedAt != null) — это повторный вызов; вернуть текущую карточку (200) без новой строки таймлайна.
  2. Проверка входа и предусловий. DealId не пустой, иначе VALIDATION_FAILED. Сделка должна быть нетерминальной (Status ∈ {Application, Booking, Sale}); терминальные Completed/CancelledCRM_DEAL_ALREADY_TERMINAL (INV-DEAL-9). Способ оплаты и бронь для отметки не обязательны — фиксируется лишь факт чтения условий.
  3. Что читаем. Сделку инвестора — по record-level предикату PrimaryInvestorUserId = actor и DeletedAt == null; чужая, несуществующая или удалённая сделка → CRM_DEAL_NOT_FOUND (существование чужой сделки не раскрывается). Условия брони/оплаты здесь не перечитываются — это display-данные карточки (get-deal.md).
  4. Бизнес-правила и переходы. Проставить TermsAcknowledgedAt = now, UpdatedByUserId = actor, UpdatedAt = now; Status/StepId не меняются. Добавляется строка таймлайна TermsAcknowledged со Status = Done, ActorRole = InvestorPrimary, Payload = TimelineEmptyPayload {} (deal-timeline-event.md, INV-TLE-2).
  5. Что меняется. В одном согласованном изменении обновляется поле отметки на сделке и добавляется строка deal_timeline_events (append-only, INV-TLE-2). Интеграционных Kafka-событий нет — отметка «Ознакомлен» приватный факт участника, консюмеров не имеет; жизненный цикл сделки публикуют другие операции (deal-created.md, deal-stage-changed.md, deal-completed.md). Параллельная попытка изменить ту же сделку → CRM_DEAL_CONCURRENCY_CONFLICT (перечитать и повторить).
  6. Результат. Обновлённая карточка DealCardModel (эскроу-номер — маска ****NNNN), 200. Повторная отметка (шаг 1) — 200 с текущей карточкой без новой строки таймлайна.

Диаграмма

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

ТипДетали
Integration eventsnone (Kafka-событие не публикуется — приватная отметка участника)
External callsnone (условия брони/оплаты карточка читает отдельно из crm.booking_programs/crm.purchase_programs)
Cache / projections / searchnone (проекция сделки не затрагивается — Title/ФИО/статус не меняются)
Notificationsопосредованно: строка таймлайна TermsAcknowledged видна участникам во вкладке «Ход сделки» (US-INV-09); отдельного пуша нет

Ошибки и failure modes

Ошибки

Главные гейты: сделка должна принадлежать актору-инвестору по record-level предикату (иначе CRM_DEAL_NOT_FOUND — без раскрытия существования чужой сделки) и быть нетерминальной (Status ∈ {Application, Booking, Sale}, иначе CRM_DEAL_ALREADY_TERMINAL, INV-DEAL-9).

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId = Guid.Empty«Некорректный идентификатор сделки»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DEALS_FORBIDDEN403JWT не инвестора (actor не является инвестором платформы)«Недостаточно прав»no retry
CRM_DEAL_NOT_FOUND404сделки нет, удалена или PrimaryInvestorUserId != actor (чужая)«Сделка не найдена»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9)«Сделка завершена или отменена»no retry
CRM_DEAL_CONCURRENCY_CONFLICT409конкурентное изменение той же сделки«Повторите попытку»retry (перечитка)
Повтор — не ошибка

Если у сделки уже есть отметка (TermsAcknowledgedAt != null), повторный вызов — не ошибка: возвращается текущая карточка (200) без новой строки таймлайна и без изменения отметки (проверяется на шаге 1 алгоритма, до бизнес-правил).

Acceptance Criteria

  • Given своя нетерминальная сделка без отметки (TermsAcknowledgedAt == null), When инвестор жмёт «Ознакомлен», Then TermsAcknowledgedAt = now, добавлена строка таймлайна TermsAcknowledged (Status = Done, ActorRole = InvestorPrimary, TimelineEmptyPayload), Status/StepId не меняются, ответ 200 с карточкой.
  • Given TermsAcknowledgedAt != null, When повтор, Then возвращается текущая карточка (200) без новой строки таймлайна и без изменения отметки.
  • Given сделка Completed/Cancelled, When acknowledge, Then CRM_DEAL_ALREADY_TERMINAL (409) по INV-DEAL-9.
  • Given PrimaryInvestorUserId != actor, сделка удалена или не существует, When acknowledge, Then CRM_DEAL_NOT_FOUND (404) — существование чужой сделки не раскрывается.
  • Given JWT не инвестора платформы, When acknowledge, Then CRM_DEALS_FORBIDDEN (403); пустой DealIdVALIDATION_FAILED (400).
  • Given параллельное изменение той же сделки, When acknowledge, Then CRM_DEAL_CONCURRENCY_CONFLICT (409); клиент перечитывает и повторяет.
  • Given успешная отметка, When карточка возвращается, Then EscrowAccountNumber — маска ****NNNN, интеграционных Kafka-событий отметка не публикует (приватный факт участника).

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

  • Пустое тело и 200-карточка стабильны; новые optional-поля DealCardModel — additive; удаление/ переименование/смена типа поля или значения enum — breaking (migration + rollout). Enum-поля (DealStatus/…) сериализуются строкой; клиент обязан толерантно обрабатывать неизвестное значение.
  • Logs: deal terms acknowledged (DealId, PrimaryInvestorUserId; ФИО/телефон/эскроу — не в логи); metrics: crm_deal_terms_acknowledged_total{result}, latency p95 (< 500 мс, SLO команд); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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