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

UC-CRM-003. Движение сделки по канбан-доске

Назначение

Пользователь перетаскивает карточку сделки DealEntity между настраиваемыми этапами одной канбан-доски застройщика (drag-n-drop). Система переводит сделку на целевой этап (INV-DEAL-5), денормализует системную группу StepGroup, при смене группы синхронизирует Status, пишет строку таймлайна StageChanged и публикует pin.crm.deal-stage-changed.v1. Дом полей сделки — ../domain/deal.md, этапов — ../domain/pipeline-step.md; здесь описаны только поток и дельта.

Metadata

ПолеЗначение
IDUC-CRM-003
Контекстcrm (см. docs/06-services/crm/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент / руководитель агентства / Менеджер продаж застройщика (в своём scope); Менеджер ПИН — кросс-tenant (UC-CRM-006)
Permissions / scopecrm.deals.manage (двигать сделку), crm.pipeline.manage (настройка доски, поток A2); агентская сторона — record-level по OriginAgencyCompanyId/ExecutorAgentUserId; застройщик — TenantId; Менеджер ПИН — platform-scope
TenantСделка и этапы: TenantId = DeveloperCompanyId (владелец доски — застройщик, ADR-0052). Агентская сторона работает в кросс-tenant режиме с явным ограничением по OriginAgencyCompanyId
Record-levelАгент — ExecutorAgentUserId = actor (руководитель агентства — весь OriginAgencyCompanyId); застройщик — свой TenantId; Менеджер ПИН — обход record-level

Предусловие

  • Сделка существует и не в терминальном статусе (Status ∉ {Completed, Cancelled}, INV-DEAL-9; ../domain/deal.md, enum DealStatus).
  • Целевой этап — живой (DeletedAt is null) этап того же застройщика (TenantId сделки), привязанный к системной группе PipelineStepGroup (../domain/pipeline-step.md).
  • Доска застройщика настроена: у застройщика есть этапы (SEED-дефолт при онбординге + настройка, поток A2).
  • Актор видит сделку по record-level/scope (агент — свою, застройщик — свой TenantId, Менеджер ПИН — любую).

Триггер

ПолеЗначение
ИсточникUI drag-n-drop → вызов API
Триггерящий элементКарточка сделки на канбан-доске /crmPUT /api/agent/crm/deals/{dealId}/step (симметрично — области developer/pin-manager)

Основной Поток

  1. Контекст и доступ. Пользователь перетаскивает карточку сделки в другой этап той же доски. Модель запроса MoveDealStepRequest { TargetStepId, CancelReason?, RowVersion? }.
  2. Проверка входа. DealId/TargetStepId заданы, длина CancelReason 1..1000 (если задан) — иначе VALIDATION_FAILED.
  3. Проверки состояния (сначала сделка — нужен её TenantId для проверки этапа):
    • Сделка существует и видна актору по record-level/scope — иначе CRM_DEAL_NOT_FOUND (существование чужих сделок не раскрывается);
    • Status ∉ {Completed, Cancelled} (INV-DEAL-9) — иначе CRM_DEAL_ALREADY_TERMINAL;
    • целевой этап живой (DeletedAt is null) и того же TenantId, что и сделка (INV-DEAL-5) — иначе CRM_DEAL_STEP_FORBIDDEN (чужие/архивные этапы не раскрываются).
  4. Бизнес-правила и переходы (INV-DEAL-5):
    • TargetStepId == StepIdno-op: вернуть текущую карточку, мутаций и событий нет;
    • денормализация StepId ← целевой этап, StepGroup ← группа этапа;
    • внутри группы (группа этапа == текущая StepGroup) — Status не меняется;
    • смена группыStatus синхронизируется по маппингу групп (../domain/pipeline-step.md): Application→Application, Booking→Booking, Contract/Payment/RegistrationSale, Cancelled→Cancelled. Guards: группы статуса Sale требуют ContractType/PaymentMethod (INV-DEAL-6) → иначе CRM_DEAL_CONTRACT_TYPE_REQUIRED; группа Completed зарезервирована за lifecycle-переходом застройщика Complete (INV-DEAL-7, поток A1); группа Cancelled требует непустой CancelReason (INV-DEAL-5) и отменяет сделку (INV-DEAL-8: активная бронь отменяется тем же изменением).
    • UpdatedByUserId ← actor, UpdatedAt ← now. События DealMovedToStep (+ DealStatusChanged при смене статуса).
  5. Что меняется. Обновляется сделка; пишется строка DealTimelineEventEntity(StageChanged) (TimelineStageChangedPayload { FromStepId, FromStepName, ToStepId, ToStepName, ToStepGroup }, ../domain/deal-timeline-event.md, INV-TLE-2). Правка не должна затирать чужую — конкурентный доступ разрешается оптимистичной блокировкой; гонка drag-n-drop → CRM_DEAL_CONFLICT. Всё — в одном согласованном изменении вместе с записью в outbox.
  6. Публикация. Интеграционное событие DealStageChangedEvent (топик pin.crm.deal-stage-changed.v1, AggregateId = DealId, AggregateType = "Deal") — через outbox. При переходе в группу Cancelled — дополнительно DealCancelledEventpin.crm.deal-cancelled.v1. Отмена сделки переводит активную бронь → Cancelled тем же согласованным изменением (INV-DEAL-8), поэтому release объекта в chessboard может прийти двумя путями (deal-cancelled и каскадный booking-cancelled) — освобождение идемпотентно (hold-guard HoldDealId == DealId, INV-CHS-003): повторный release безопасен, объект «Свободен» ставится один раз; конкурентный worker booking-expiration на уже отменённой брони — no-op (терминальный статус вне выборки).
  7. Результат. Возвращается карточка сделки (StepId, StepGroup, Status, UpdatedAt, свежий RowVersion); карточка занимает новую колонку канбана. Асинхронно: messenger рисует системную карточку «этап изменён» в чате сделки, notifications уведомляет участников.

Диаграмма

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

A1. Авто-переход при бронировании / продаже

  1. Создание брони (../domain/booking.md, UC-BKG-001) авто-переводит Application → Booking; оплата/подтверждение брони — StartSale (Booking → Sale, INV-DEAL-6); истечение брони воркером booking-expirationBooking → Application; завершение застройщиком (../api/developer/complete-deal.md) — Sale → Completed.
  2. Каждый переход публикует тот же DealStageChangedEvent (ChangedByUserId = null для системных авто-переходов) и пишет строку таймлайна.
Точка схождения с основным потоком

Сходится с основным потоком на шаге 6; drag-n-drop актором не требуется (карточка двигается автоматически).

A2. Настройка доски застройщиком

  1. Менеджер застройщика (crm.pipeline.manage) создаёт/переименовывает/перекрашивает, меняет порядок и архивирует этапы: ../api/developer/create-step.md, update-step.md, reorder-steps.md, archive-step.md.
  2. Системные группы PipelineStepGroup (SEED) неизменны через API (INV-STEP-1/5); каждый этап обязан быть привязан к группе; при архиве этапа с активными сделками они переносятся в этап-приёмник той же группы (INV-STEP-4) — StepGroup/Status сделок не меняются, событие наружу не публикуется.
Связь с основным потоком

Меняет только справочник этапов; напрямую в основной поток не входит (обеспечивает предусловие «доска настроена»).

A3. Ручной кросс-tenant перевод Менеджером ПИН

  1. Менеджер ПИН при конфликте/ошибке вручную двигает этап/статус любой сделки платформы (platform-scope, обход record-level) — ../api/pin-manager/move-deal-step.md, UC-CRM-006 (US-MGR-03).
  2. Публикуется тот же DealStageChangedEvent (ChangedByUserId = менеджер ПИН); в таймлайн добавляется запись перехода (при арбитраже — ManualNote, INV-TLE-5).
Точка схождения с основным потоком

Сходится с основным потоком на шаге 6.

Постусловие

  • DealEntity.StepId/StepGroupStatus при смене группы) обновлены; UpdatedAt/UpdatedByUserId проставлены; при переходе в CancelledCancelledAt/CancelReason, активная бронь отменена.
  • В таймлайне сделки — строка StageChanged (+ StatusChanged/DealCancelled при смене статуса/отмене).
  • В outbox — pin.crm.deal-stage-changed.v1 (+ pin.crm.deal-cancelled.v1 при переходе в Cancelled); no-op повтор событий не публикует.
  • Карточка в новой колонке канбана; messenger и notifications доведены eventual.

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

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400DealId/TargetStepId пусты или длина CancelReason вне 1..1000Исправить запрос
CRM_DEAL_NOT_FOUND404Сделки нет или она вне scope актора (существование не раскрывается)Перечитать доску
CRM_DEAL_STEP_FORBIDDEN422Целевой этап чужого TenantId/архивный; группа Cancelled без CancelReason; группа Completed через drag-n-drop (INV-DEAL-5/7)Выбрать корректный этап / указать причину
CRM_DEAL_CONTRACT_TYPE_REQUIRED409Переход в группу статуса Sale без ContractType/PaymentMethod (INV-DEAL-6)Указать тип договора и способ оплаты
CRM_DEAL_ALREADY_TERMINAL409Сделка Completed/Cancelled (INV-DEAL-9)Сделка завершена, действие недоступно
CRM_DEAL_CONFLICT409Гонка drag-n-drop (оптимистичная блокировка)Перечитать доску и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
DealMovedToStepПеревод сделки на этап (смена StepId)Тот же обработчик: строка DealTimelineEventEntity(StageChanged)
DealStatusChangedСмена Status при переходе в другую группуТот же обработчик: строка таймлайна StatusChanged/DealCancelled; сборка DealStageChangedEvent

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
DealStageChangedEventpin.crm.deal-stage-changed.v1Любое изменение StepId/Status (drag-n-drop, авто-переход, ручной перевод); кроме no-opmessenger (карточка «этап изменён»), notifications (уведомление участникам) — ../events/deal-stage-changed.md
DealCancelledEventpin.crm.deal-cancelled.v1Переход в группу Cancelled (INV-DEAL-8)chessboard (освобождение объекта недвижимости), notifications, rewards../events/deal-cancelled.md

Acceptance Criteria

#КритерийПроверка
AC1Перетаскивание в этап той же группы меняет StepId, не трогает Status/StepGroupТест PUT .../step → 200, StepId новый, Status/StepGroup прежние
AC2Перетаскивание в этап другой группы синхронизирует Status по маппингу группТест: Contract-этап → Status = Sale, StepGroup = Contract
AC3Целевой этап чужого TenantId или архивный → CRM_DEAL_STEP_FORBIDDEN (INV-DEAL-5)Негативный тест
AC4Переход в группу Cancelled без CancelReasonCRM_DEAL_STEP_FORBIDDEN; с CancelReason — сделка Cancelled, активная бронь отмененаПозитивный/негативный тесты, проверка CancelReason/CancelledAt
AC5Публикуется ровно одно pin.crm.deal-stage-changed.v1 (кроме no-op); при отмене — дополнительно deal-cancelled.v1Тест outbox
AC6Каждый переход добавляет строку StageChanged в таймлайн (INV-TLE-2, append-only)Тест crm.deal_timeline_events
AC7Терминальная сделка → CRM_DEAL_ALREADY_TERMINAL; гонка конкурентной правки → CRM_DEAL_CONFLICTНегативные тесты

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

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

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