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

UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение

Назначение

Финальное оформление сделки после того, как бронь сыграла: менеджер застройщика проводит сделку DealEntity строго по этапам договоры подготовлены → (эскроу для ДДУ) → Росреестр → завершение (Sale → Completed), инвестор указывает эскроу-счёт. Дом полей и инвариантов сделки — ../domain/deal.md (INV-DEAL-6/7/9); здесь описаны только поток и дельта.

Metadata

ПолеЗначение
IDUC-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 заданы; для InstallmentInstallmentPresetId (../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 происходит только на завершающем шаге.

  1. Договоры подготовлены (менеджер). POST /api/developer/crm/deals/{id}/mark-contracts-prepared. Проверки: сделка существует, Status = Sale и не терминальна (INV-DEAL-9); tenant совпадает (deal.TenantId == actor.TenantId); INV-DEAL-6ContractType/PaymentMethod заданы (иначе CRM_DEAL_CONTRACT_TYPE_REQUIRED); идемпотентность — ContractsPreparedAt is null.
  2. Что меняется. ContractsPreparedAt = now; событие DealContractsPrepared. Пишется DealTimelineEventEntity(ContractsPrepared) (payload TimelineContractsPreparedPayload { ContractType }) — в одном согласованном изменении. Уведомление инвестору «Договоры подготовлены» (US-INV-20) доставляет notifications по pin.crm.document-status-changed.v1 от загрузки SaleContract.
  3. Эскроу (инвестор, только 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 (payload TimelineEscrowPayload { BankId?, AccountNumberMasked }только маска ****NNNN, Sensitive). Порядок относительно шага 1 свободен; строгий гейт — на шаге 4.
  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).
  5. Завершение (менеджер). 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).
  6. Публикация. Интеграционное событие 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) для карточки чата.
  7. Согласованность. Агрегат + таймлайн + записи в outbox фиксируются атомарно в одном согласованном изменении. Возвращаются DealId, Status = Completed, CompletedAt.
  8. Downstream (async). investor-owner заводит PropertyObjectId в портфель InvestorUserId; rewards фиксирует комиссию агенту/агентству (база FinalPrice); chessboard переводит объект недвижимости в «Продан»; notifications уведомляет стороны.

Диаграмма

Альтернативные Потоки

A1. Отказ по сделке (Cancel)

  1. Любая сторона (агент/менеджер/инвестор/Менеджер ПИН) инициирует отказ: POST .../deals/{id}/cancel { CancelReason }. CancelReason обязателен (check ck_deals_cancel_reason) — иначе ValidationError.
  2. INV-DEAL-8: отказ из нетерминального статуса (Application/Booking/Sale); активная бронь отменяется тем же изменением. Переход * → Cancelled, CancelledAt = now, CancelReason сохранён. Таймлайн DealCancelled (TimelineCancelledPayload { Reason }).
  3. Публикуется DealCancelledEvent (pin.crm.deal-cancelled.v1): chessboard освобождает объект недвижимости, notifications уведомляет участников (../events/deal-cancelled.md).
Терминальность отказа

Отказ терминален; в основной поток сделка не возвращается.

A2. Нарушение порядка этапов (INV-DEAL-7)

CRM_DEAL_STAGE_ORDER_VIOLATION
  1. Менеджер вызывает confirm-rosreestr без ContractsPreparedAt (или для Ddu без EscrowStatus = Provided), либо complete без RosreestrRegisteredAt.
  2. CRM_DEAL_STAGE_ORDER_VIOLATION (409); метка/статус не меняются.

Возврат: выполнить пропущенный предыдущий этап, затем повторить с нужного шага основного потока.

A3. Терминальная сделка

CRM_DEAL_ALREADY_TERMINAL
  1. Команда любого этапа приходит на сделку в Completed/Cancelled.
  2. CRM_DEAL_ALREADY_TERMINAL (409); операция игнорируется (INV-DEAL-9). Completed/Cancelled эмитятся не более одного раза на DealId.

Постусловие

  • Deal.Status = Completed (терминальна, иммутабельна — INV-DEAL-9); проставлены ContractsPreparedAt, RosreestrRegisteredAt, CompletedAt; для DduEscrowStatus = Provided.
  • В таймлайне сделки — упорядоченная цепочка ContractsPreparedEscrowProvided (для Ddu) → RosreestrRegisteredDealCompleted, все Status = Done.
  • В outbox — ровно одно pin.crm.deal-completed.v1; downstream (eventual): объект в портфеле investor-owner, комиссия в rewards, объект недвижимости «Продан» в chessboard.
  • Альтернатива A1: Status = Cancelled, CancelReason задан, объект недвижимости освобождён консюмером chessboard.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
CRM_DEAL_CONTRACT_TYPE_REQUIRED422ContractType/PaymentMethod не заданы на шаге договоров (INV-DEAL-6)Заполнить тип договора/способ оплаты в карточке
CRM_DEAL_STAGE_ORDER_VIOLATION409Росреестр без договоров/эскроу(Ddu) или завершение без Росреестра (INV-DEAL-7)Выполнить пропущенный предыдущий этап
CRM_DEAL_ALREADY_TERMINAL409Команда этапа/отказа по Completed/Cancelled (INV-DEAL-9)Действие недоступно; обновить карточку
ValidationError (CancelReason)422Отказ без CancelReason (A1, check ck_deals_cancel_reason)Форма требует указать причину отказа
CONCURRENCY_CONFLICT409Проигрыш гонки конкурентной правки по сделке (оптимистичная блокировка)Перечитать карточку и повторить

Доступ вне 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
DealCompletedEventpin.crm.deal-completed.v1Шаг завершения (Sale → Completed), через outbox атомарноinvestor-owner (портфель), rewards (комиссия), chessboard (объект недвижимости «Продан»), notifications../events/deal-completed.md
DealStageChangedEventpin.crm.deal-stage-changed.v1Смена Status/этапа (StartSale, Complete)messenger (карточка в чат), notifications../events/deal-stage-changed.md
DocumentStatusChangedEventpin.crm.document-status-changed.v1Загрузка SaleContract (US-DEV-03) — уведомление «Договоры подготовлены» (US-INV-20)messenger, notifications../events/document-status-changed.md
DealCancelledEventpin.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.v1key = DealId, ContractVersion = 1at-least-once, порядок per-deal, ровно один раз на DealId (терминальность INV-DEAL-9)investor-owner, rewards, chessboard, notifications
pin.crm.deal-stage-changed.v1key = DealId, v1на StartSale и Complete (смена Status/этапа)messenger (карточка в чат), notifications
pin.crm.deal-cancelled.v1key = DealId, v1на отказ A1, один раз на переход * → Cancelledchessboard, notifications, rewards
pin.crm.document-status-changed.v1key = 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 не передаётся.
Промежуточное состояние (окно eventual-consistency)

После успешного 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-seen DealId). Транзиентная ошибка консюмера — повторные попытки; после исчерпания — сообщение в DLQ и ручной разбор (объект в портфеле / комиссия / «Продан» появятся после восстановления). Само завершение сделки в crm из-за падения downstream не откатывается.
  • Компенсации. У завершения компенсаций нет (терминально, необратимо). Прекращение до терминальности — отдельный бизнес-поток A1 (Cancel), а не компенсация завершения.

Acceptance Criteria

#КритерийПроверка
AC1Полный порядок договоры → эскроу(Ddu) → Росреестр → завершение проставляет метки и даёт Status = CompletedИнтеграционный тест 4 команд → 200; поля ContractsPreparedAt/RosreestrRegisteredAt/CompletedAt заданы
AC2confirm-rosreestr для Ddu без EscrowStatus = ProvidedCRM_DEAL_STAGE_ORDER_VIOLATION (409)Негативный тест INV-DEAL-7
AC3complete без RosreestrRegisteredAtCRM_DEAL_STAGE_ORDER_VIOLATION (409)Негативный тест INV-DEAL-7
AC4provide-escrow инвестором пишет полный номер в БД, в таймлайн/наружу — только маска ****NNNNТест: EscrowStatus = Provided, payload TimelineEscrowPayload.AccountNumberMasked замаскирован
AC5Завершение публикует ровно одно pin.crm.deal-completed.v1 с FinalPrice = Deal.PriceТест outbox + контракт ../events/deal-completed.md
AC6Повторная команда по Completed/CancelledCRM_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-ов этапов.

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

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

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