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 area | developer |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/mark-contracts-prepared.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика; сквозной доступ — PinManager, platform-scope) |
| Auth policy / Permissions | crm.deals.decide (JWT identity) |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | сделка принадлежит TenantId актора (INV-DEAL-7: маркер ставит только менеджер застройщика сделки); Менеджер ПИН — кросс-tenant |
| Tenant isolation | TenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически |
| Audit | update (lifecycle): ContractsPreparedAt, UpdatedByUserId; PII не пишется в логи |
Маркер ставит только менеджер застройщика этой сделки (TenantId = DeveloperCompanyId, permission
crm.deals.decide); Менеджер ПИН имеет кросс-tenant доступ (platform-scope).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/deals/{dealId}/contracts-prepared |
| Success status | 200 (обновлённая карточка сделки) |
| Idempotency header | не применяется (бизнес-идемпотентность по ContractsPreparedAt, шаг 1) |
| Correlation | сквозной trace через заголовок traceparent |
Тело запроса пустое (маркер без полезной нагрузки); команда собирается из route + identity.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (DeveloperCompanyId) | Tenant isolation |
DealId | route | Guid | Да | != Guid.Empty; существующая сделка TenantId (шаг 2) | — |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика | пишется в UpdatedByUserId/таймлайн |
Модель ответа DealCardModel
Возвращается обновлённая карточка сделки (проекция карточки /crm/deals/[id]). Дом полей — сущность
DealEntity; ниже — только дельта, изменяемая этой операцией. Списочные модели здесь
не участвуют (перечни сделок — см. list-deals.md).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id | — | Идентификатор сделки. |
Status | DealStatus | Да | DealEntity.Status (enum строкой) | — | Остаётся Sale (маркер — внутри статуса). |
StepId | Guid | Да | DealEntity.StepId | — | Новый этап канбана (первый активный этап следующей системной группы). |
StepGroup | PipelineStepGroup | Да | DealEntity.StepGroup (enum строкой) | — | Системная группа после перехода (Payment/Registration). |
ContractType | DealContractType | Да | DealEntity.ContractType (enum строкой) | — | Ddu/Dkp (обязателен, INV-DEAL-6). |
ContractsPreparedAt | DateTimeOffset | Да | DealEntity.ContractsPreparedAt | — | Проставленный момент отметки. |
EscrowStatus | EscrowStatus | Да | DealEntity.EscrowStatus (enum строкой) | — | None/Provided; подсказка к следующему шагу (для Ddu). |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | US-INV-04. |
Остальные поля карточки — как в deal.md; все enum — строкой,
EscrowAccountNumber наружу маскируется (****NNNN).
Предусловия и постусловия
Предусловия (должны быть истинны до вызова):
- сделка существует в
TenantIdактора и находится в статусеSale, системная группа этапа —Contract; - заданы
ContractType(Ddu/Dkp) иPaymentMethod(иInstallmentPresetIdприPaymentMethod = Installment) — INV-DEAL-6; - порядок маркеров оформления соблюдён:
RosreestrRegisteredAt == nullиCompletedAt == null(договоры — первый из трёх маркеров, INV-DEAL-7); - актор — менеджер застройщика с
crm.deals.decide(или менеджер ПИН, platform-scope).
Договоры — первый из трёх маркеров оформления: договоры → (для Ddu — эскроу Provided) → Росреестр →
завершение. Если Росреестр или завершение уже отмечены раньше договоров — 409 CRM_DEAL_STAGE_ORDER_VIOLATION.
Постусловия при успехе:
ContractsPreparedAt = now;StepId/StepGroupпродвинуты в следующую системную группу (Payment, при отсутствии —Registration);StatusосталсяSale; версия инкрементирована;- в таймлайн добавлена строка
ContractsPrepared(Status = Done); - опубликовано
DealStageChangedEvent→pin.crm.deal-stage-changed.v1(через outbox).
Если маркер уже стоял (Росреестр/завершение ещё нет) — возвращается текущая карточка 200 без повторного
продвижения этапа и без нового события (бизнес-идемпотентность по ContractsPreparedAt).
Постусловия при отказе: сделка не изменилась (этап, ContractsPreparedAt, статус — прежние), таймлайн не
пополнялся, событие не публиковалось.
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.deals.decide, иначеCRM_DEALS_FORBIDDEN) либо менеджер ПИН (platform-scope); tenant —TenantId = DeveloperCompanyId. Бизнес-идемпотентность: если у сделки ужеContractsPreparedAt != null, аRosreestrRegisteredAt == nullиCompletedAt == null— это повтор маркера; вернуть текущую карточку200без переиздания события. - Проверка входа и предусловий.
DealId != Guid.Empty— иначеVALIDATION_FAILED;- сделка существует в tenant застройщика — иначе
CRM_DEAL_NOT_FOUND(чужой tenant не раскрывается); - состояние
Status == Sale(подготовка договора начата) — иначеCRM_DEAL_STAGE_ORDER_VIOLATION; терминальныеCompleted/Cancelled→CRM_DEAL_ALREADY_TERMINAL; - обязательность реквизитов (INV-DEAL-6): заданы
ContractTypeиPaymentMethod(иInstallmentPresetIdприPaymentMethod = Installment) — иначеCRM_DEAL_CONTRACT_TYPE_REQUIRED; - порядок (INV-DEAL-7):
RosreestrRegisteredAt == nullиCompletedAt == null(договоры — первый маркер) — иначеCRM_DEAL_STAGE_ORDER_VIOLATION. Эскроу здесь не проверяется — это guard следующего маркера (Росреестр).
- Что читаем. Сделку — с защитой от гонки решений сторон. Целевой этап продвижения — первый активный этап
следующей системной группы после
Contract(pipeline-step.md): группаPayment(илиRegistration, еслиPaymentу застройщика не сконфигурирован). - Бизнес-правила и переходы (INV-DEAL-7). Маппинг поле-в-поле:
ContractsPreparedAt = now,StepId = целевой этап,StepGroup = группа целевого этапа(денормализация),Statusбез изменений (Sale),UpdatedByUserId = ActorUserId,UpdatedAt = now. Фиксируется доменный фактDealStageChanged; его подписчик материализует строку таймлайнаContractsPrepared(Status = Done,Payload = TimelineContractsPreparedPayload { ContractType }, deal-timeline-event.md). - Что меняется. Сделка получает
ContractsPreparedAtи продвигается в следующую системную группу; в ленту добавляется строкаContractsPrepared. Конкурентный доступ — по оптимистической версии: параллельная попытка →CRM_DEAL_CONCURRENCY_CONFLICT(перечитка и повтор). - События. Интеграционное
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(карточка этапа в чате сделки). - Согласованность. Обновление сделки, строка таймлайна и событие фиксируются в одном согласованном изменении (атомарно, INV-TLE-2).
- Результат. Свежая карточка
DealCardModel(маскированиеEscrowAccountNumber) → HTTP200. Повтор маркера (шаг 1) —200с текущей карточкой без нового события.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | DealStageChangedEvent → pin.crm.deal-stage-changed.v1 (переход этапа внутри Sale), outbox |
| External calls | none (данные — своя схема crm; этап — справочник crm.pipeline_steps) |
| Cache / projections / search | none (проекция сделки DealSearchDocument не затрагивается — Title/ФИО/статус не меняются) |
| Notifications | опосредованно: notifications (консюмер deal-stage-changed) — пуш/карточка инвестору «Договоры подготовлены» (US-INV-20); messenger — системная карточка «этап сделки изменён» в чате сделки |
Интеграционная последовательность и окно согласованности
- Синхронно (в одном изменении): сделка продвинута, строка таймлайна создана, событие положено в outbox —
ответ
200менеджеру. Карточка сделки у застройщика сразу показывает новый этап. - Асинхронно:
deal-stage-changed.v1доставляется потребителям через outbox-диспетчер:notificationsформирует инвестору сообщение «Договоры подготовлены» (US-INV-20);messengerдобавляет в чат сделки системную карточку смены этапа.
- Окно eventual-consistency = задержка доставки outbox → консюмер: до её завершения инвестор ещё не видит
уведомление/карточку, тогда как застройщик уже наблюдает новый этап. Порядок событий по одной сделке
сохраняется ключом партиционирования
AggregateId = DealId; повторная доставка обрабатывается потребителями идемпотентно по(AggregateId, ContractVersion, ChangedAt).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId = Guid.Empty | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_DEALS_FORBIDDEN | 403 | нет permission crm.deals.decide | «Недостаточно прав» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки нет или чужой TenantId | «Сделка не найдена» | no retry |
CRM_DEAL_CONTRACT_TYPE_REQUIRED | 409 | не заданы ContractType/PaymentMethod (INV-DEAL-6) | «Укажите тип договора и способ оплаты» | no retry |
CRM_DEAL_STAGE_ORDER_VIOLATION | 409 | Status != Sale или Росреестр/завершение уже отмечены (INV-DEAL-7) | «Нарушен порядок этапов оформления» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка Completed/Cancelled (INV-DEAL-9) | «Сделка завершена или отменена» | no retry |
CRM_DEAL_CONCURRENCY_CONFLICT | 409 | параллельная правка сделки (конфликт по версии) | «Повторите попытку» | 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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Acceptance criteria
- Given сделка в
Sale, группаContract, заданыContractType/PaymentMethod, Росреестр/завершение не отмечены, When менеджер отмечает «Договоры подготовлены», Then200:ContractsPreparedAtпроставлен, этап продвинут вPayment/Registration, статусSale, опубликованоdeal-stage-changed.v1. - Given маркер уже стоял (Росреестр/завершение ещё нет), When повторный вызов, Then
200с текущей карточкой, без нового события и без повторного продвижения этапа (идемпотентность поContractsPreparedAt). - Given не заданы
ContractType/PaymentMethod, When вызов, Then409 CRM_DEAL_CONTRACT_TYPE_REQUIRED(INV-DEAL-6). - Given
Status != Sale, либо Росреестр/завершение уже отмечены, When вызов, Then409 CRM_DEAL_STAGE_ORDER_VIOLATION(INV-DEAL-7). - Given сделка
Completed/Cancelled, When вызов, Then409 CRM_DEAL_ALREADY_TERMINAL. - Given параллельная правка сделки (конфликт версии), When вызов, Then
409 CRM_DEAL_CONCURRENCY_CONFLICT(перечитать и повторить). - Given сделка чужого
TenantId/несуществующая, When вызов, Then404 CRM_DEAL_NOT_FOUND; безcrm.deals.decide—403 CRM_DEALS_FORBIDDEN.
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal.md (дом полей,
DealStatus/DealContractType/EscrowStatus, методMarkContractsPrepared, инварианты INV-DEAL-6/7/9), deal-timeline-event.md (ContractsPrepared,TimelineContractsPreparedPayload), pipeline-step.md (PipelineStepGroup, порядок этапов), deal-document.md (SaleContract). - События: deal-stage-changed.md (публикуемый контракт); соседние маркеры оформления — deal-completed.md; статусы документов — document-status-changed.md.
- Контекст: README; соседняя операция области: list-deals.md; tenancy — ADR-0052.