POST /api/developer/crm/deals/{dealId}/complete — Завершить сделку
Менеджер застройщика из карточки сделки (/crm/deals/[dealId]) подтверждает завершение сделки после
регистрации в Росреестре: переход Sale → Completed (в UI «продажа (завершена)»). Дом полей сделки —
deal.md (метод Complete, INV-DEAL-7/9). Это финальный шаг цепочки застройщика:
договоры → эскроу (Ddu) → Росреестр → завершение.
Назначение
По кнопке «Завершить сделку» менеджер застройщика переводит сделку в терминальный статус Completed
(проставляется CompletedAt, в ленту пишется DealCompleted). На выходе — обновлённая карточка сделки.
Публикуется интеграционное событие deal-completed, по которому смежные
контексты доводят продажу до конца: investor-owner заводит объект в портфель инвестора, rewards начисляет
комиссию, chessboard переводит объект недвижимости в «Продан».
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer (JWT менеджера застройщика) |
| Feature group | deals |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/complete-deal.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, арбитраж) |
| Auth policy / Permissions | crm.deals.decide (терминальное решение стороны застройщика, ТЗ.md:391–409) |
| Scope (RBAC) | организация (застройщик): tenant + record-level по сделке |
| Record-level | актор — SellManager/менеджер застройщика сделки (deal.TenantId) либо Менеджер ПИН |
| Tenant isolation | TenantId = DeveloperCompanyId из claims; сделка читается только в этом tenant (deal.TenantId == actor.TenantId, ADR-0052) |
| Audit | update-terminal (UpdatedByUserId = actor.UserId, CompletedAt); бизнес-история — таймлайн DealCompleted; ФИО/эскроу в логи не пишутся (только DealId) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/deals/{dealId}/complete |
| Success status | 200 (обновлённая карточка сделки) |
| Idempotency header | не применяется (естественная идемпотентность: повтор на терминале → CRM_DEAL_ALREADY_TERMINAL, событие — ровно один раз) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId актора | Tenant isolation |
DealId | route | Guid | Да | != Guid.Empty; существующая сделка в tenant; статус Sale + Росреестр подтверждён (шаг 2) | — |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика/Менеджера ПИН; пишется в CompletedAt-актора (UpdatedByUserId) | — |
Тело запроса пустое (подтверждение без параметров): все данные события — snapshot полей агрегата на момент завершения. Вложенных сущностей/коллекций нет.
Модель ответа DealCardModel
Проекция завершённой сделки (дом полей — deal.md; та же модель, что у карточки сделки и смены объекта — ниже только отдаваемая дельта операции).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealEntity.Id | — | Идентификатор сделки. |
Title | string | Да | DealEntity.Title | — | Заголовок карточки. |
Status | string (enum DealStatus) | Да | DealEntity.Status | — | Completed — «продажа (завершена)». |
StepId | Guid | Да | DealEntity.StepId | — | Текущий этап канбана (завершением не меняется). |
PropertyObjectId | Guid | Да | DealEntity.PropertyObjectId | — | Объект сделки (→ «Продан» в chessboard). |
ProjectId | Guid | Да | DealEntity.ProjectId | — | Проект объекта (реплика chessboard). |
Price | MoneyModel | Да | DealEntity.Price | — | Финальная стоимость ({Amount, Currency}). |
ContractType | string (enum DealContractType) | Да | DealEntity.ContractType | — | Ddu/Dkp (обязателен с группы Sale, INV-DEAL-6). |
CompletedAt | DateTimeOffset | Да | DealEntity.CompletedAt | — | Момент подтверждения завершения. |
UpdatedAt | DateTimeOffset | Да | DealEntity.UpdatedAt | — | US-INV-04. |
Списки сделок застройщика — отдельные операции; здесь ответ скалярный (одна карточка).
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (JWT) либо менеджер ПИН (platform-scope) с permission
crm.deals.decide; tenant —TenantId = DeveloperCompanyId. Естественная идемпотентность: повтор на терминале отклоняется (шаг 2), событие публикуется ровно один раз. - Проверка входа и предусловий.
DealId != Guid.Empty— иначеVALIDATION_FAILED;- сделка существует в tenant застройщика — иначе
CRM_DEAL_NOT_FOUND(чужой tenant не раскрывается: 404); - доступ: актор — менеджер застройщика этого tenant (record-level) либо менеджер ПИН — иначе
CRM_DEAL_FORBIDDEN; - терминальность (INV-DEAL-9):
Status ∈ {Completed, Cancelled}→CRM_DEAL_ALREADY_TERMINAL(терминал иммутабелен); - порядок стадий (INV-DEAL-7):
Status == SaleиRosreestrRegisteredAt != null(регистрация в Росреестре подтверждена) — иначеCRM_DEAL_STAGE_ORDER_VIOLATION(дляApplication/Bookingпродажа ещё не начата).
- Что читаем. Сделку — для перехода; гонку с
move-step/cancel/сменой объекта разрешает версия строки. Внешних вызовов нет: все поля события (PrimaryInvestorUserId,CommissionRecipientUserId=ExecutorAgentUserIdна момент завершения,OriginAgentUserId,OriginAgencyCompanyId,PropertyObjectId,ProjectId,Price,ContractType) — снимок на агрегате с момента создания/перехода вSale. - Бизнес-правила и переходы (INV-DEAL-7/9). Завершение проверяет guard:
Status == SaleиRosreestrRegisteredAt != null(иначеCRM_DEAL_STAGE_ORDER_VIOLATION), сделка не терминальна (иначеCRM_DEAL_ALREADY_TERMINAL); переходSale → Completed; маппинг:Status = Completed,CompletedAt = now,UpdatedAt = now,UpdatedByUserId = ActorUserId. Фиксируется доменный фактDealStatusChanged (From: Sale, To: Completed); в той же транзакции материализуется запись ленты (INV-TLE-2):DealTimelineEventEntity(Type = DealCompleted, Status = Done, OccurredAt = now, CompletedAt = now, ActorRole = SellManager, Payload = TimelineEmptyPayload)— дом типов/payload deal-timeline-event.md. - Что меняется. Сделка переходит в
Completed(+CompletedAt), в ленту добавляется append-only строкаDealCompleted(INV-TLE-1). Конкурентный доступ — по версии строки: параллельныйCancel/MoveToStepне должен затирать завершение → при гонке ретрай, терминальный статус даётCRM_DEAL_ALREADY_TERMINAL. - События. Интеграционное
DealCompletedEvent→ топикpin.crm.deal-completed.v1(ContractVersion = 1, ключ =DealId); payload — снимок агрегата:DealId,TenantId,PrimaryInvestorUserId,CommissionRecipientUserId(=ExecutorAgentUserIdна момент завершения — получатель комиссии),OriginAgentUserId,OriginAgencyCompanyId(иммутабельная атрибуция — основание начислений),PropertyObjectId,ProjectId,FinalPrice {Amount, Currency},ContractType,CompletedAt(дом — deal-completed.md). Публикуется ровно один раз (Completedтерминален, INV-DEAL-9). Потребители:investor-owner(объект в портфель),rewards(комиссия),chessboard(объект → «Продан»). - Согласованность. Обновление сделки, строка таймлайна и событие фиксируются в одном согласованном изменении (атомарно).
- Результат. Карточка
DealCardModel(Status = Completed,CompletedAt,Price,ContractType,UpdatedAt) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | DealCompletedEvent → pin.crm.deal-completed.v1 (ровно один на сделку), outbox |
| External calls | none (все поля события — snapshot на агрегате; статус объекта недвижимости меняет chessboard как консюмер) |
| Cache / projections / search | deals.Status → Completed (ix_deals_status); лента таймлайна получает строку DealCompleted; объект недвижимости PropertyObjectId в chessboard → «Продан», объект в портфель investor-owner, комиссия rewards — всё через консюмеров события |
| Notifications | опосредованно: notifications — консюмер deal-completed (уведомление сторонам о завершении) и TimelineItemAdded (US-INV-11) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат DealId | «Проверьте корректность запроса» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделка не найдена в tenant застройщика | «Сделка не найдена» | no retry |
CRM_DEAL_FORBIDDEN | 403 | сделка чужого застройщика / нет прав | «Нет доступа к сделке» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | Status ∈ {Completed, Cancelled} (INV-DEAL-9) | «Сделка уже завершена или отменена» | no retry |
CRM_DEAL_STAGE_ORDER_VIOLATION | 409 | Status != Sale либо RosreestrRegisteredAt == null (INV-DEAL-7: Росреестр не подтверждён) | «Завершение недоступно: не пройдены предыдущие этапы» | no retry |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика сделки (
SellManager) либо Менеджер ПИН с permissioncrm.deals.decide; - сделка существует в
TenantId = DeveloperCompanyIdактора и не терминальна (INV-DEAL-9); - пройдены предыдущие стадии (INV-DEAL-7):
Status == SaleиRosreestrRegisteredAt != null(регистрация в Росреестре подтверждена).
Постусловия (гарантированы после успеха):
- сделка переведена
Sale → Completed, проставленCompletedAt,UpdatedByUserId/UpdatedAt; - в ленту добавлена append-only строка
DealCompleted(INV-TLE-1); Completedтерминален и иммутабелен (INV-DEAL-9) — событиеdeal-completed.v1уходит в outbox ровно один раз;StepIdзавершением не меняется.
Интеграционная последовательность (после коммита)
DealCompletedEvent → pin.crm.deal-completed.v1 (ключ = DealId, снимок агрегата: PrimaryInvestorUserId,
CommissionRecipientUserId (= ExecutorAgentUserId на момент завершения), OriginAgentUserId,
OriginAgencyCompanyId, PropertyObjectId, ProjectId, FinalPrice, ContractType, CompletedAt)
обрабатывается асинхронно после коммита. В ответе 200 актор видит Completed сразу, но эффекты соседей
доходят с задержкой (окно eventual-consistency — секунды):
investor-owner— заводит объект недвижимости в портфель инвестора;rewards— начисляет комиссию по завершённой сделке;chessboard— переводит объект недвижимости в статус «Продан»;notifications— уведомляет стороны о завершении.
Что этот сервис предоставляет: deal-completed.v1 — additive-only, exactly-once по терминальному переходу.
Что ожидает: ничего синхронного — все поля события уже на агрегате (внешних вызовов на пути нет). В
промежуточном окне объект в шахматке ещё «Бронь»/«Продажа», портфель инвестора ещё не пополнен, комиссия не
начислена — это ожидаемое eventual-consistency состояние, а не ошибка.
Acceptance Criteria
- Happy path. Дано: сделка
Saleс подтверждённым Росреестром. Когда: менеджер завершает. Тогда:200,Status = Completed,CompletedAtпроставлен, строка лентыDealCompleted,deal-completed.v1опубликовано один раз. - Стадия не Sale. Дано: сделка в
Application/Booking. Когда: завершение. Тогда:409 CRM_DEAL_STAGE_ORDER_VIOLATION, событий нет. - Росреестр не подтверждён. Дано:
Status == Sale, ноRosreestrRegisteredAt == null. Когда: завершение. Тогда:409 CRM_DEAL_STAGE_ORDER_VIOLATION(INV-DEAL-7). - Уже терминальна / повтор. Дано:
Status ∈ {Completed, Cancelled}. Когда: завершение. Тогда:409 CRM_DEAL_ALREADY_TERMINAL; событие повторно не публикуется. - Гонка с отменой. Дано: параллельный
Cancel/MoveToStep. Когда: конфликт версии строки. Тогда: проигравший ретраится; на терминале получаетCRM_DEAL_ALREADY_TERMINAL(завершение не затирается отменой). - Чужой застройщик. Дано: сделка другого tenant. Когда: завершение. Тогда:
404 CRM_DEAL_NOT_FOUND.
Совместимость и наблюдаемость
- Тело запроса пустое; ответ
DealCardModel— новые optional поля additive без breaking; удаление/ переименование/смена типа поля илиSuccess status— breaking (migration + rollout). Unknown enumDealStatus/DealContractTypeв ответе не возникают (сервер — источник). Контракт события — additive-only (deal-completed.md). - Logs:
deal completed(DealId,TenantId,PropertyObjectId; сумма/ФИО/эскроу — не пишутся); metricscrm_deals_completed_total{contract_type}, latency p95 < 500 мс; traces — сквозной trace черезtraceparent; dashboard Grafanapin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/developer/crm/deals/{dealId}/rosreestr — Подтвердить регистрацию Росреестра, POST /api/pin-manager/crm/deals/{dealId}/override-status — Ручная смена статуса сделки
- Use Cases (1): UC-CRM-003. Движение сделки по канбан-доске
- Саги (1): Saga «Deal Completion» (fan-out завершения сделки)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal.md (дом полей, метод
Complete, INV-DEAL-7/9,DealStatus), deal-timeline-event.md (DealCompleted,TimelineEmptyPayload). - Событие: deal-completed.md (
pin.crm.deal-completed.v1— портфельinvestor-owner, комиссияrewards, объект недвижимости «Продан»chessboard, уведомления). Соседние операции стадий: confirm-booking-documents.md; карта областей — README контекста. - ADR: 0052 tenancy, 0056 simplicity-first.