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 area | investor |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/acknowledge-terms.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Инвестор — участник сделки) |
| Auth policy / Permissions | authenticated (JWT инвестора); отдельного RBAC-permission не требуется — self-service отметка |
| Scope (RBAC) | не применяется: инвестор — вне developer-tenant (кросс-tenant участник, ADR-0052) |
| Record-level | DealEntity.PrimaryInvestorUserId = actor.UserId — форсится сервером; чужую сделку отметить нельзя |
| Tenant isolation | не developer-tenanted: Deal.TenantId = DeveloperCompanyId ≠ актор; изоляция — record-level по PrimaryInvestorUserId из claims (ADR-0052) |
| Audit | update (lifecycle): TermsAcknowledgedAt, UpdatedByUserId; ФИО/телефон/эскроу в логи не пишутся |
Отметить может только сам инвестор — участник сделки; PrimaryInvestorUserId форсится сервером из токена
актора и не принимается из тела/маршрута. Чужая, несуществующая или удалённая сделка неотличимы снаружи —
всегда CRM_DEAL_NOT_FOUND.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investor/crm/deals/{dealId}/acknowledge-terms |
| Success status | 200 (обновлённая карточка сделки) |
| Idempotency header | не применяется (бизнес-идемпотентность по TermsAcknowledgedAt) |
| Correlation | сквозной trace через заголовок traceparent |
Тело запроса пустое (маркер без полезной нагрузки); операция определяется по маршруту и токену инвестора.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route | Guid | Да | != Guid.Empty; существующая сделка актора-инвестора | — |
PrimaryInvestorUserId | identity claims | Guid | Да | Required; record-level предикат, форсится = actor.UserId (из body/route не берётся) | scope isolation |
Модель ответа DealCardModel
Возвращается обновлённая карточка сделки (проекция карточки /profile/deals/[dealId]). Дом модели —
get-deal.md (DealCardModel), дом полей — сущность DealEntity.
Ниже — только дельта, изменяемая этой операцией; остальные поля карточки — как в доме.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | DealEntity.Id | — | Идентификатор сделки. |
Status | DealStatus | Да | DealEntity.Status (enum строкой) | — | Без изменений (отметка не меняет статус): Application/Booking/Sale. |
TermsAcknowledgedAt | DateTimeOffset? | Нет | DealEntity.TermsAcknowledgedAt | — | Проставленный момент отметки «Ознакомлен» (US-INV-18). |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | Дата последнего обновления (US-INV-04). |
Все enum — строкой; EscrowAccountNumber в остальных полях карточки наружу маскируется (****NNNN, дом —
deal.md).
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор — участник сделки;
PrimaryInvestorUserIdберётся из его токена (не из запроса). Бизнес-идемпотентность: если у сделки уже проставлена отметка (TermsAcknowledgedAt != null) — это повторный вызов; вернуть текущую карточку (200) без новой строки таймлайна. - Проверка входа и предусловий.
DealIdне пустой, иначеVALIDATION_FAILED. Сделка должна быть нетерминальной (Status ∈ {Application, Booking, Sale}); терминальныеCompleted/Cancelled→CRM_DEAL_ALREADY_TERMINAL(INV-DEAL-9). Способ оплаты и бронь для отметки не обязательны — фиксируется лишь факт чтения условий. - Что читаем. Сделку инвестора — по record-level предикату
PrimaryInvestorUserId = actorиDeletedAt == null; чужая, несуществующая или удалённая сделка →CRM_DEAL_NOT_FOUND(существование чужой сделки не раскрывается). Условия брони/оплаты здесь не перечитываются — это display-данные карточки (get-deal.md). - Бизнес-правила и переходы. Проставить
TermsAcknowledgedAt = now,UpdatedByUserId = actor,UpdatedAt = now;Status/StepIdне меняются. Добавляется строка таймлайнаTermsAcknowledgedсоStatus = Done,ActorRole = InvestorPrimary,Payload = TimelineEmptyPayload {}(deal-timeline-event.md, INV-TLE-2). - Что меняется. В одном согласованном изменении обновляется поле отметки на сделке и добавляется строка
deal_timeline_events(append-only, INV-TLE-2). Интеграционных Kafka-событий нет — отметка «Ознакомлен» приватный факт участника, консюмеров не имеет; жизненный цикл сделки публикуют другие операции (deal-created.md, deal-stage-changed.md, deal-completed.md). Параллельная попытка изменить ту же сделку →CRM_DEAL_CONCURRENCY_CONFLICT(перечитать и повторить). - Результат. Обновлённая карточка
DealCardModel(эскроу-номер — маска****NNNN),200. Повторная отметка (шаг 1) —200с текущей карточкой без новой строки таймлайна.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (Kafka-событие не публикуется — приватная отметка участника) |
| External calls | none (условия брони/оплаты карточка читает отдельно из crm.booking_programs/crm.purchase_programs) |
| Cache / projections / search | none (проекция сделки не затрагивается — 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_FAILED | 400 | DealId = Guid.Empty | «Некорректный идентификатор сделки» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_DEALS_FORBIDDEN | 403 | JWT не инвестора (actor не является инвестором платформы) | «Недостаточно прав» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки нет, удалена или PrimaryInvestorUserId != actor (чужая) | «Сделка не найдена» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) | «Сделка завершена или отменена» | no retry |
CRM_DEAL_CONCURRENCY_CONFLICT | 409 | конкурентное изменение той же сделки | «Повторите попытку» | retry (перечитка) |
Если у сделки уже есть отметка (TermsAcknowledgedAt != null), повторный вызов — не ошибка: возвращается
текущая карточка (200) без новой строки таймлайна и без изменения отметки (проверяется на шаге 1
алгоритма, до бизнес-правил).
Acceptance Criteria
- Given своя нетерминальная сделка без отметки (
TermsAcknowledgedAt == null), When инвестор жмёт «Ознакомлен», ThenTermsAcknowledgedAt = now, добавлена строка таймлайнаTermsAcknowledged(Status = Done,ActorRole = InvestorPrimary,TimelineEmptyPayload),Status/StepIdне меняются, ответ200с карточкой. - Given
TermsAcknowledgedAt != null, When повтор, Then возвращается текущая карточка (200) без новой строки таймлайна и без изменения отметки. - Given сделка
Completed/Cancelled, When acknowledge, ThenCRM_DEAL_ALREADY_TERMINAL(409) по INV-DEAL-9. - Given
PrimaryInvestorUserId != actor, сделка удалена или не существует, When acknowledge, ThenCRM_DEAL_NOT_FOUND(404) — существование чужой сделки не раскрывается. - Given JWT не инвестора платформы, When acknowledge, Then
CRM_DEALS_FORBIDDEN(403); пустойDealId→VALIDATION_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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal.md (дом полей,
DealStatus,TermsAcknowledgedAt, инвариант INV-DEAL-9), deal-timeline-event.md (TermsAcknowledged,TimelineEmptyPayload), booking-program.md и purchase-program.md (условия брони/оплаты, которые читает инвестор). - Модель ответа
DealCardModel(дом): get-deal.md; соседняя операция области: list-deals.md. - События жизненного цикла сделки: deal-created.md, deal-stage-changed.md, deal-completed.md.
- Контекст: README; tenancy/record-level — ADR-0052.