UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение
Назначение
Финальное оформление сделки после того, как бронь сыграла: менеджер застройщика проводит сделку DealEntity
строго по этапам договоры подготовлены → (эскроу для ДДУ) → Росреестр → завершение (Sale → Completed),
инвестор указывает эскроу-счёт. Дом полей и инвариантов сделки — ../domain/deal.md
(INV-DEAL-6/7/9); здесь описаны только поток и дельта.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-005 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной (+ альтернативный поток отказа) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Менеджер застройщика (основной) — этапы договоры/Росреестр/завершение; Инвестор — шаг эскроу (US-INV-19) |
| Permissions / scope | Менеджер: crm.deals.decide; Инвестор: crm.deals.manage, record-level по своей сделке (без decide) |
| Tenant | Менеджер: TenantId = DeveloperCompanyId (ADR-0052); Инвестор: null-tenant, record-level PrimaryInvestorUserId = actor |
| Record-level | Менеджер — свой TenantId; Инвестор — активный участник InvestorPrimary сделки |
Предусловие
- Сделка существует и
Status = Sale— бронь оплачена/подтверждена, произошёлStartSale(Booking → Sale, ../domain/booking.md; UC-BKG-004). - INV-DEAL-6 уже выполнен на переходе
StartSale:ContractType(Ddu/Dkp) иPaymentMethodзаданы; дляInstallment—InstallmentPresetId(../domain/deal.md). - Подготовленные договоры загружены менеджером как
DealDocumentEntity(SaleContract)(US-DEV-03, ../domain/deal-document.md) — клиенту доступно скачивание (US-INV-20). - Инвестор ознакомлен с условиями (
TermsAcknowledgedAt, US-INV-18) — предпосылка к вводу эскроу.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI-действие → вызов API (каждый этап — отдельная операция) |
| Триггерящий элемент | Карточка сделки (застройщик/инвестор): кнопки «Договоры подготовлены», «Указать эскроу», «Подтвердить Росреестр», «Завершить сделку» |
Основной Поток
Все переходы этого потока — внутри Status = Sale и проставляют временные метки строго в порядке
INV-DEAL-7; смена Status происходит только на завершающем шаге.
- Договоры подготовлены (менеджер).
POST /api/developer/crm/deals/{id}/mark-contracts-prepared. Проверки: сделка существует,Status = Saleи не терминальна (INV-DEAL-9); tenant совпадает (deal.TenantId == actor.TenantId); INV-DEAL-6 —ContractType/PaymentMethodзаданы (иначеCRM_DEAL_CONTRACT_TYPE_REQUIRED); идемпотентность —ContractsPreparedAt is null. - Что меняется.
ContractsPreparedAt = now; событиеDealContractsPrepared. ПишетсяDealTimelineEventEntity(ContractsPrepared)(payloadTimelineContractsPreparedPayload { ContractType }) — в одном согласованном изменении. Уведомление инвестору «Договоры подготовлены» (US-INV-20) доставляетnotificationsпоpin.crm.document-status-changed.v1от загрузкиSaleContract. - Эскроу (инвестор, только
Ddu).POST /api/investor/crm/deals/{id}/provide-escrow{ EscrowAccountNumber }(record-level:deal.PrimaryInvestorUserId == actor). Проверки:Status = Sale,ContractType == Ddu. ЗаписываетсяEscrowAccountNumber(открытым в БД),EscrowStatus = Provided(US-INV-19); событиеDealEscrowProvided; таймлайнEscrowProvided(payloadTimelineEscrowPayload { BankId?, AccountNumberMasked }— только маска****NNNN, Sensitive). Порядок относительно шага 1 свободен; строгий гейт — на шаге 4. - Росреестр (менеджер).
POST /api/developer/crm/deals/{id}/confirm-rosreestr. INV-DEAL-7:Status = Sale,ContractsPreparedAt is not nullи (ContractType == Ddu ⇒ EscrowStatus == Provided) — иначеCRM_DEAL_STAGE_ORDER_VIOLATION. ЗаписываетсяRosreestrRegisteredAt = now; событиеDealRosreestrRegistered; таймлайнRosreestrRegistered(TimelineEmptyPayload). - Завершение (менеджер).
POST /api/developer/crm/deals/{id}/complete. INV-DEAL-7/9:Status = Sale,RosreestrRegisteredAt is not null(иначеCRM_DEAL_STAGE_ORDER_VIOLATION). ПереходSale → Completed,CompletedAt = now; событияDealCompleted+DealStatusChanged; таймлайнDealCompleted(TimelineEmptyPayload). - Публикация. Интеграционное событие
DealCompletedEvent(топикpin.crm.deal-completed.v1,AggregateId = DealId) собирается из полей агрегата (PrimaryInvestorUserId,CommissionRecipientUserId(=Executorна момент завершения),OriginAgencyCompanyId,PropertyObjectId,ProjectId,Price → FinalPrice,ContractType,CompletedAt) — через outbox. Смена этапа канбана также даётDealStageChangedEvent(pin.crm.deal-stage-changed.v1) для карточки чата. - Согласованность. Агрегат + таймлайн + записи в outbox фиксируются атомарно в одном согласованном
изменении. Возвращаются
DealId,Status = Completed,CompletedAt. - Downstream (async).
investor-ownerзаводитPropertyObjectIdв портфельInvestorUserId;rewardsфиксирует комиссию агенту/агентству (базаFinalPrice);chessboardпереводит объект недвижимости в «Продан»;notificationsуведомляет стороны.
Диаграмма
Альтернативные Потоки
A1. Отказ по сделке (Cancel)
- Любая сторона (агент/менеджер/инвестор/Менеджер ПИН) инициирует отказ:
POST .../deals/{id}/cancel{ CancelReason }.CancelReasonобязателен (checkck_deals_cancel_reason) — иначеValidationError. - INV-DEAL-8: отказ из нетерминального статуса (
Application/Booking/Sale); активная бронь отменяется тем же изменением. Переход* → Cancelled,CancelledAt = now,CancelReasonсохранён. ТаймлайнDealCancelled(TimelineCancelledPayload { Reason }). - Публикуется
DealCancelledEvent(pin.crm.deal-cancelled.v1):chessboardосвобождает объект недвижимости,notificationsуведомляет участников (../events/deal-cancelled.md).
Отказ терминален; в основной поток сделка не возвращается.
A2. Нарушение порядка этапов (INV-DEAL-7)
- Менеджер вызывает
confirm-rosreestrбезContractsPreparedAt(или дляDduбезEscrowStatus = Provided), либоcompleteбезRosreestrRegisteredAt. CRM_DEAL_STAGE_ORDER_VIOLATION(409); метка/статус не меняются.
Возврат: выполнить пропущенный предыдущий этап, затем повторить с нужного шага основного потока.
A3. Терминальная сделка
- Команда любого этапа приходит на сделку в
Completed/Cancelled. CRM_DEAL_ALREADY_TERMINAL(409); операция игнорируется (INV-DEAL-9).Completed/Cancelledэмитятся не более одного раза наDealId.
Постусловие
Deal.Status = Completed(терминальна, иммутабельна — INV-DEAL-9); проставленыContractsPreparedAt,RosreestrRegisteredAt,CompletedAt; дляDdu—EscrowStatus = Provided.- В таймлайне сделки — упорядоченная цепочка
ContractsPrepared→EscrowProvided(дляDdu) →RosreestrRegistered→DealCompleted, всеStatus = Done. - В outbox — ровно одно
pin.crm.deal-completed.v1; downstream (eventual): объект в портфелеinvestor-owner, комиссия вrewards, объект недвижимости «Продан» вchessboard. - Альтернатива A1:
Status = Cancelled,CancelReasonзадан, объект недвижимости освобождён консюмеромchessboard.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
CRM_DEAL_CONTRACT_TYPE_REQUIRED | 422 | ContractType/PaymentMethod не заданы на шаге договоров (INV-DEAL-6) | Заполнить тип договора/способ оплаты в карточке |
CRM_DEAL_STAGE_ORDER_VIOLATION | 409 | Росреестр без договоров/эскроу(Ddu) или завершение без Росреестра (INV-DEAL-7) | Выполнить пропущенный предыдущий этап |
CRM_DEAL_ALREADY_TERMINAL | 409 | Команда этапа/отказа по Completed/Cancelled (INV-DEAL-9) | Действие недоступно; обновить карточку |
ValidationError (CancelReason) | 422 | Отказ без CancelReason (A1, check ck_deals_cancel_reason) | Форма требует указать причину отказа |
CONCURRENCY_CONFLICT | 409 | Проигрыш гонки конкурентной правки по сделке (оптимистичная блокировка) | Перечитать карточку и повторить |
Доступ вне scope/record-level (не тот tenant / не участник) — 403 до применения guard-ов.
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DealContractsPrepared | Шаг «Договоры подготовлены» | Тот же обработчик → DealTimelineEventEntity(ContractsPrepared) |
DealEscrowProvided | Шаг «Эскроу» (US-INV-19) | Таймлайн EscrowProvided (только маска) |
DealRosreestrRegistered | Шаг «Росреестр» | Таймлайн RosreestrRegistered |
DealCompleted / DealStatusChanged | Завершение (Sale → Completed) | Таймлайн DealCompleted; сборка DealCompletedEvent |
DealCancelled / DealStatusChanged | Отказ (A1, INV-DEAL-8) | Таймлайн DealCancelled; сборка DealCancelledEvent |
Все доменные события материализуются в таймлайн подписчиком в том же согласованном изменении (INV-TLE-2, ../domain/deal-timeline-event.md).
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DealCompletedEvent | pin.crm.deal-completed.v1 | Шаг завершения (Sale → Completed), через outbox атомарно | investor-owner (портфель), rewards (комиссия), chessboard (объект недвижимости «Продан»), notifications — ../events/deal-completed.md |
DealStageChangedEvent | pin.crm.deal-stage-changed.v1 | Смена Status/этапа (StartSale, Complete) | messenger (карточка в чат), notifications — ../events/deal-stage-changed.md |
DocumentStatusChangedEvent | pin.crm.document-status-changed.v1 | Загрузка SaleContract (US-DEV-03) — уведомление «Договоры подготовлены» (US-INV-20) | messenger, notifications — ../events/document-status-changed.md |
DealCancelledEvent | pin.crm.deal-cancelled.v1 | Поток A1 (Cancel) | chessboard (освободить объект недвижимости), notifications — ../events/deal-cancelled.md |
Промежуточные этапы (ContractsPrepared/EscrowProvided/RosreestrRegistered) собственных Kafka-событий не
публикуют — только доменные события + таймлайн; уведомления US-INV-11 доставляет notifications по
TimelineItemAdded.
Кросс-сервисные ожидания и гарантии
Оформление сделки затрагивает несколько контекстов; шаги 1–5 основного потока — синхронные HTTP-команды к
crm, downstream (шаг 8) — асинхронные событийные цепочки.
Что crm предоставляет (исходящие гарантии):
| Контракт | Ключ / версия | Гарантия | Кому |
|---|---|---|---|
pin.crm.deal-completed.v1 | key = DealId, ContractVersion = 1 | at-least-once, порядок per-deal, ровно один раз на DealId (терминальность INV-DEAL-9) | investor-owner, rewards, chessboard, notifications |
pin.crm.deal-stage-changed.v1 | key = DealId, v1 | на StartSale и Complete (смена Status/этапа) | messenger (карточка в чат), notifications |
pin.crm.deal-cancelled.v1 | key = DealId, v1 | на отказ A1, один раз на переход * → Cancelled | chessboard, notifications, rewards |
pin.crm.document-status-changed.v1 | key = DealId, v1 | при загрузке SaleContract (US-DEV-03) | messenger, notifications |
Что crm ожидает от соседей (входные гарантии/реплики):
chessboard— владелец статуса объекта недвижимости (решение 5 README): поdeal-completedпереводит объект в «Продан», поdeal-cancelled— освобождает.crmсинхронного ответа не ждёт;PropertyObjectId— логическая ссылка. Повторная доставка не должна повторно менять статус (first-seen поDealId).messenger— владелец чата (ADR-0055); на сделке — только проекцияChatStatus, обновляемая консюмеромpin.messenger.deal-chat-status-changed.v1. Гейты оформления от готовности чата не зависят.investor-owner/rewards— заводят объект в портфель / комиссию с базойFinalPrice = Deal.Price(first-seen поDealId); идемпотентны; окно доставки — см. «Наблюдаемость и НФТ».notifications— резолвит ФИО/контакты сторон по собственной репликеidentity; raw-PII в событияхcrmне передаётся.
После успешного complete карточка сделки сразу Completed, таймлайн закрыт. Появление объекта в «Мои
объекты» инвестора (investor-owner), комиссии у агента (rewards) и статуса «Продан» в шахматке
(chessboard) — с задержкой доставки; до этого пользователь видит завершённую сделку, но downstream-эффекты
ещё «в пути» — это ожидаемо, повторных команд не требуется.
Идемпотентность, конкурентность и восстановление
- Идемпотентность по этапам. Каждый этап защищён гейтом целевой метки: повтор
mark-contracts-preparedприContractsPreparedAt is not null— no-op (успех без изменения); повторconfirm-rosreestr/completeпо уже проставленной метке — no-op; любая команда по терминальной сделке —CRM_DEAL_ALREADY_TERMINAL(INV-DEAL-9). Повторprovide-escrowприStatus = Saleперезаписывает номер и оставляетEscrowStatus = Provided(совместимо с повтором). - Конкурентность. Параллельные команды по одной сделке (например менеджер
confirm-rosreestrи инвесторprovide-escrow) разрешаются оптимистичной блокировкой: первый коммит выигрывает, проигравший получаетCONCURRENCY_CONFLICT(409) и перечитывает карточку. Порядок эскроу относительно договоров свободен — строгий гейт INV-DEAL-7 применяется только наconfirm-rosreestr. - Доставка downstream (retry/DLQ).
deal-completed/deal-cancelledпубликуются через outbox (at-least-once) и не теряются при падении консюмера; консюмеры идемпотентны (MessageId+ first-seenDealId). Транзиентная ошибка консюмера — повторные попытки; после исчерпания — сообщение в DLQ и ручной разбор (объект в портфеле / комиссия / «Продан» появятся после восстановления). Само завершение сделки вcrmиз-за падения downstream не откатывается. - Компенсации. У завершения компенсаций нет (терминально, необратимо). Прекращение до терминальности —
отдельный бизнес-поток A1 (
Cancel), а не компенсация завершения.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Полный порядок договоры → эскроу(Ddu) → Росреестр → завершение проставляет метки и даёт Status = Completed | Интеграционный тест 4 команд → 200; поля ContractsPreparedAt/RosreestrRegisteredAt/CompletedAt заданы |
| AC2 | confirm-rosreestr для Ddu без EscrowStatus = Provided → CRM_DEAL_STAGE_ORDER_VIOLATION (409) | Негативный тест INV-DEAL-7 |
| AC3 | complete без RosreestrRegisteredAt → CRM_DEAL_STAGE_ORDER_VIOLATION (409) | Негативный тест INV-DEAL-7 |
| AC4 | provide-escrow инвестором пишет полный номер в БД, в таймлайн/наружу — только маска ****NNNN | Тест: EscrowStatus = Provided, payload TimelineEscrowPayload.AccountNumberMasked замаскирован |
| AC5 | Завершение публикует ровно одно pin.crm.deal-completed.v1 с FinalPrice = Deal.Price | Тест outbox + контракт ../events/deal-completed.md |
| AC6 | Повторная команда по Completed/Cancelled → CRM_DEAL_ALREADY_TERMINAL, событие не дублируется | Негативный тест INV-DEAL-9 |
| AC7 | Отказ (A1) требует CancelReason, даёт pin.crm.deal-cancelled.v1, объект недвижимости освобождается | Тест cancel + консюмер chessboard |
Наблюдаемость и НФТ
- Логи (без PII/секретов): по каждому этапу —
DealId,TenantId, actor, тип этапа, исход (успех / код ошибки).EscrowAccountNumberв открытом виде и суммы сделки в логах запрещены (эскроу — только маска****NNNN). - Метрики: число завершённых сделок; время прохождения цепочки договоры→эскроу→Росреестр→завершение
(перцентили); доля
CRM_DEAL_STAGE_ORDER_VIOLATIONиCRM_DEAL_ALREADY_TERMINAL; доля отказов (A1) с распределением по причине (агрегировано, без текста причины); лаг доставкиdeal-completedдо downstream. - Латентность/нагрузка: этапные команды интерактивны — целевой P95 в пределах сотен мс при низкой нагрузке (ADR-0056: сделки — низкообъёмная сущность); downstream-окно — минуты (SLA консюмеров — в контрактах событий).
- Доступ: менеджер застройщика —
crm.deals.decideв своёмTenantId(этапы договоры/Росреестр/ завершение); инвестор —crm.deals.manage, record-level по своей сделке (только шаг эскроу); нарушение scope/record-level —403до применения guard-ов этапов.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-009. Передача ведения сделки (смена исполнителя)
- scenarios (3): UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор), UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (будущие пути по конвенции):
../api/developer-mark-contracts-prepared.md,../api/investor-provide-escrow.md,../api/developer-confirm-rosreestr.md,../api/developer-complete-deal.md,../api/developer-cancel-deal.md,../api/developer-upload-sale-contract.md. - Domain entity: ../domain/deal.md (INV-DEAL-6/7/8/9, enum
DealStatus/EscrowStatus, поля этапов), ../domain/deal-document.md (SaleContract), ../domain/deal-timeline-event.md (типыContractsPrepared/EscrowProvided/RosreestrRegistered/DealCompleted/DealCancelled). - Kafka события: ../events/deal-completed.md, ../events/deal-cancelled.md, ../events/deal-stage-changed.md, ../events/document-status-changed.md.
- Смежные UC: UC-CRM-002 (создание сделки),
UC-BKG-004 (оплата брони →
StartSale), UC-CRM-006 (ручные статусы); US-реестр — user-stories-deal.md (US-INV-18/19/20, US-DEV-04/06/07). - ADR: 0052 tenancy, 0056 simplicity-first.