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

POST /api/developer/crm/deals/{dealId}/contracts-prepared — Отметить «Договоры подготовлены»

Менеджер застройщика подтверждает готовность договоров по сделке (US-DEV-04; для инвестора отображается «Договоры подготовлены», US-INV-20). Первый из трёх лайфсайкл-маркеров стадии оформления сделки (DealEntity, INV-DEAL-7: договоры → эскроу → Росреестр → завершение). Проставляет ContractsPreparedAt, продвигает карточку на следующий этап канбана в статусе Sale, пишет таймлайн-событие ContractsPrepared и публикует DealStageChangedEvent.

Назначение

Дать менеджеру застройщика (SellManager) отметить, что договоры (ДДУ/ДКП) по сделке сформированы и готовы к подписанию клиентом (US-DEV-04/US-INV-20). Операция — доменный переход внутри статуса Sale (группа этапов Contract → следующая), а не загрузка файла: сам договор загружается отдельным документом SaleContract (deal-document.md).

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/developer/mark-contracts-prepared.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика; сквозной доступ — PinManager, platform-scope)
Auth policy / Permissionscrm.deals.decide (JWT identity)
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelсделка принадлежит TenantId актора (INV-DEAL-7: маркер ставит только менеджер застройщика сделки); Менеджер ПИН — кросс-tenant
Tenant isolationTenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически
Auditupdate (lifecycle): ContractsPreparedAt, UpdatedByUserId; PII не пишется в логи
Доступ

Маркер ставит только менеджер застройщика этой сделки (TenantId = DeveloperCompanyId, permission crm.deals.decide); Менеджер ПИН имеет кросс-tenant доступ (platform-scope).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/deals/{dealId}/contracts-prepared
Success status200 (обновлённая карточка сделки)
Idempotency headerне применяется (бизнес-идемпотентность по ContractsPreparedAt, шаг 1)
Correlationсквозной trace через заголовок traceparent

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (DeveloperCompanyId)Tenant isolation
DealIdrouteGuidДа!= Guid.Empty; существующая сделка TenantId (шаг 2)
ActorUserIdidentity claimsGuidДаJWT менеджера застройщикапишется в UpdatedByUserId/таймлайн

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

Возвращается обновлённая карточка сделки (проекция карточки /crm/deals/[id]). Дом полей — сущность DealEntity; ниже — только дельта, изменяемая этой операцией. Списочные модели здесь не участвуют (перечни сделок — см. list-deals.md).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealEntity.IdИдентификатор сделки.
StatusDealStatusДаDealEntity.Status (enum строкой)Остаётся Sale (маркер — внутри статуса).
StepIdGuidДаDealEntity.StepIdНовый этап канбана (первый активный этап следующей системной группы).
StepGroupPipelineStepGroupДаDealEntity.StepGroup (enum строкой)Системная группа после перехода (Payment/Registration).
ContractTypeDealContractTypeДаDealEntity.ContractType (enum строкой)Ddu/Dkp (обязателен, INV-DEAL-6).
ContractsPreparedAtDateTimeOffsetДаDealEntity.ContractsPreparedAtПроставленный момент отметки.
EscrowStatusEscrowStatusДаDealEntity.EscrowStatus (enum строкой)None/Provided; подсказка к следующему шагу (для Ddu).
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtUS-INV-04.

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

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

Предусловия (должны быть истинны до вызова):

  • сделка существует в TenantId актора и находится в статусе Sale, системная группа этапа — Contract;
  • заданы ContractType (Ddu/Dkp) и PaymentMethodInstallmentPresetId при PaymentMethod = Installment) — INV-DEAL-6;
  • порядок маркеров оформления соблюдён: RosreestrRegisteredAt == null и CompletedAt == null (договоры — первый из трёх маркеров, INV-DEAL-7);
  • актор — менеджер застройщика с crm.deals.decide (или менеджер ПИН, platform-scope).
Строгий порядок маркеров (INV-DEAL-7)

Договоры — первый из трёх маркеров оформления: договоры → (для Ddu — эскроу Provided) → Росреестр → завершение. Если Росреестр или завершение уже отмечены раньше договоров — 409 CRM_DEAL_STAGE_ORDER_VIOLATION.

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

  • ContractsPreparedAt = now; StepId/StepGroup продвинуты в следующую системную группу (Payment, при отсутствии — Registration); Status остался Sale; версия инкрементирована;
  • в таймлайн добавлена строка ContractsPrepared (Status = Done);
  • опубликовано DealStageChangedEventpin.crm.deal-stage-changed.v1 (через outbox).
Идемпотентность повторного вызова

Если маркер уже стоял (Росреестр/завершение ещё нет) — возвращается текущая карточка 200 без повторного продвижения этапа и без нового события (бизнес-идемпотентность по ContractsPreparedAt).

Постусловия при отказе: сделка не изменилась (этап, ContractsPreparedAt, статус — прежние), таймлайн не пополнялся, событие не публиковалось.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.deals.decide, иначе CRM_DEALS_FORBIDDEN) либо менеджер ПИН (platform-scope); tenant — TenantId = DeveloperCompanyId. Бизнес-идемпотентность: если у сделки уже ContractsPreparedAt != null, а RosreestrRegisteredAt == null и CompletedAt == null — это повтор маркера; вернуть текущую карточку 200 без переиздания события.
  2. Проверка входа и предусловий.
    • DealId != Guid.Empty — иначе VALIDATION_FAILED;
    • сделка существует в tenant застройщика — иначе CRM_DEAL_NOT_FOUND (чужой tenant не раскрывается);
    • состояние Status == Sale (подготовка договора начата) — иначе CRM_DEAL_STAGE_ORDER_VIOLATION; терминальные Completed/CancelledCRM_DEAL_ALREADY_TERMINAL;
    • обязательность реквизитов (INV-DEAL-6): заданы ContractType и PaymentMethodInstallmentPresetId при PaymentMethod = Installment) — иначе CRM_DEAL_CONTRACT_TYPE_REQUIRED;
    • порядок (INV-DEAL-7): RosreestrRegisteredAt == null и CompletedAt == null (договоры — первый маркер) — иначе CRM_DEAL_STAGE_ORDER_VIOLATION. Эскроу здесь не проверяется — это guard следующего маркера (Росреестр).
  3. Что читаем. Сделку — с защитой от гонки решений сторон. Целевой этап продвижения — первый активный этап следующей системной группы после Contract (pipeline-step.md): группа Payment (или Registration, если Payment у застройщика не сконфигурирован).
  4. Бизнес-правила и переходы (INV-DEAL-7). Маппинг поле-в-поле: ContractsPreparedAt = now, StepId = целевой этап, StepGroup = группа целевого этапа (денормализация), Status без изменений (Sale), UpdatedByUserId = ActorUserId, UpdatedAt = now. Фиксируется доменный факт DealStageChanged; его подписчик материализует строку таймлайна ContractsPrepared (Status = Done, Payload = TimelineContractsPreparedPayload { ContractType }, deal-timeline-event.md).
  5. Что меняется. Сделка получает ContractsPreparedAt и продвигается в следующую системную группу; в ленту добавляется строка ContractsPrepared. Конкурентный доступ — по оптимистической версии: параллельная попытка → CRM_DEAL_CONCURRENCY_CONFLICT (перечитка и повтор).
  6. События. Интеграционное DealStageChangedEvent → топик pin.crm.deal-stage-changed.v1 (ContractVersion = 1, AggregateId = DealId, AggregateType = "Deal"); payload: FromStepId, ToStepId, FromStepGroup = Contract, ToStepGroup, FromStatus = ToStatus = Sale, ChangedByUserId = ActorUserId, ChangedAt (дом — deal-stage-changed.md); через outbox. Отдельного топика у маркера нет. Потребители: notifications (инвестору «Договоры подготовлены», US-INV-20), messenger (карточка этапа в чате сделки).
  7. Согласованность. Обновление сделки, строка таймлайна и событие фиксируются в одном согласованном изменении (атомарно, INV-TLE-2).
  8. Результат. Свежая карточка DealCardModel (маскирование EscrowAccountNumber) → HTTP 200. Повтор маркера (шаг 1) — 200 с текущей карточкой без нового события.

Диаграмма

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

ТипДетали
Integration eventsDealStageChangedEventpin.crm.deal-stage-changed.v1 (переход этапа внутри Sale), outbox
External callsnone (данные — своя схема crm; этап — справочник crm.pipeline_steps)
Cache / projections / searchnone (проекция сделки DealSearchDocument не затрагивается — Title/ФИО/статус не меняются)
Notificationsопосредованно: notifications (консюмер deal-stage-changed) — пуш/карточка инвестору «Договоры подготовлены» (US-INV-20); messenger — системная карточка «этап сделки изменён» в чате сделки

Интеграционная последовательность и окно согласованности

  1. Синхронно (в одном изменении): сделка продвинута, строка таймлайна создана, событие положено в outbox — ответ 200 менеджеру. Карточка сделки у застройщика сразу показывает новый этап.
  2. Асинхронно: deal-stage-changed.v1 доставляется потребителям через outbox-диспетчер:
    • notifications формирует инвестору сообщение «Договоры подготовлены» (US-INV-20);
    • messenger добавляет в чат сделки системную карточку смены этапа.
  3. Окно eventual-consistency = задержка доставки outbox → консюмер: до её завершения инвестор ещё не видит уведомление/карточку, тогда как застройщик уже наблюдает новый этап. Порядок событий по одной сделке сохраняется ключом партиционирования AggregateId = DealId; повторная доставка обрабатывается потребителями идемпотентно по (AggregateId, ContractVersion, ChangedAt).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId = Guid.Empty«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DEALS_FORBIDDEN403нет permission crm.deals.decide«Недостаточно прав»no retry
CRM_DEAL_NOT_FOUND404сделки нет или чужой TenantId«Сделка не найдена»no retry
CRM_DEAL_CONTRACT_TYPE_REQUIRED409не заданы ContractType/PaymentMethod (INV-DEAL-6)«Укажите тип договора и способ оплаты»no retry
CRM_DEAL_STAGE_ORDER_VIOLATION409Status != Sale или Росреестр/завершение уже отмечены (INV-DEAL-7)«Нарушен порядок этапов оформления»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка Completed/Cancelled (INV-DEAL-9)«Сделка завершена или отменена»no retry
CRM_DEAL_CONCURRENCY_CONFLICT409параллельная правка сделки (конфликт по версии)«Повторите попытку»retry (перечитка)

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

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

Acceptance criteria

  • Given сделка в Sale, группа Contract, заданы ContractType/PaymentMethod, Росреестр/завершение не отмечены, When менеджер отмечает «Договоры подготовлены», Then 200: ContractsPreparedAt проставлен, этап продвинут в Payment/Registration, статус Sale, опубликовано deal-stage-changed.v1.
  • Given маркер уже стоял (Росреестр/завершение ещё нет), When повторный вызов, Then 200 с текущей карточкой, без нового события и без повторного продвижения этапа (идемпотентность по ContractsPreparedAt).
  • Given не заданы ContractType/PaymentMethod, When вызов, Then 409 CRM_DEAL_CONTRACT_TYPE_REQUIRED (INV-DEAL-6).
  • Given Status != Sale, либо Росреестр/завершение уже отмечены, When вызов, Then 409 CRM_DEAL_STAGE_ORDER_VIOLATION (INV-DEAL-7).
  • Given сделка Completed/Cancelled, When вызов, Then 409 CRM_DEAL_ALREADY_TERMINAL.
  • Given параллельная правка сделки (конфликт версии), When вызов, Then 409 CRM_DEAL_CONCURRENCY_CONFLICT (перечитать и повторить).
  • Given сделка чужого TenantId/несуществующая, When вызов, Then 404 CRM_DEAL_NOT_FOUND; без crm.deals.decide403 CRM_DEALS_FORBIDDEN.

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

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

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