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
| Поле | Значение |
|---|---|
| ID | UC-CRM-003 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент / руководитель агентства / Менеджер продаж застройщика (в своём scope); Менеджер ПИН — кросс-tenant (UC-CRM-006) |
| Permissions / scope | crm.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, enumDealStatus). - Целевой этап — живой (
DeletedAt is null) этап того же застройщика (TenantIdсделки), привязанный к системной группеPipelineStepGroup(../domain/pipeline-step.md). - Доска застройщика настроена: у застройщика есть этапы (SEED-дефолт при онбординге + настройка, поток A2).
- Актор видит сделку по record-level/scope (агент — свою, застройщик — свой
TenantId, Менеджер ПИН — любую).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI drag-n-drop → вызов API |
| Триггерящий элемент | Карточка сделки на канбан-доске /crm → PUT /api/agent/crm/deals/{dealId}/step (симметрично — области developer/pin-manager) |
Основной Поток
- Контекст и доступ. Пользователь перетаскивает карточку сделки в другой этап той же доски. Модель
запроса
MoveDealStepRequest { TargetStepId, CancelReason?, RowVersion? }. - Проверка входа.
DealId/TargetStepIdзаданы, длинаCancelReason1..1000(если задан) — иначеVALIDATION_FAILED. - Проверки состояния (сначала сделка — нужен её
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(чужие/архивные этапы не раскрываются).
- Сделка существует и видна актору по record-level/scope — иначе
- Бизнес-правила и переходы (INV-DEAL-5):
TargetStepId == StepId→ no-op: вернуть текущую карточку, мутаций и событий нет;- денормализация
StepId ← целевой этап,StepGroup ← группа этапа; - внутри группы (
группа этапа == текущая StepGroup) —Statusне меняется; - смена группы —
Statusсинхронизируется по маппингу групп (../domain/pipeline-step.md):Application→Application,Booking→Booking,Contract/Payment/Registration→Sale,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при смене статуса).
- Что меняется. Обновляется сделка; пишется строка
DealTimelineEventEntity(StageChanged)(TimelineStageChangedPayload { FromStepId, FromStepName, ToStepId, ToStepName, ToStepGroup }, ../domain/deal-timeline-event.md, INV-TLE-2). Правка не должна затирать чужую — конкурентный доступ разрешается оптимистичной блокировкой; гонка drag-n-drop →CRM_DEAL_CONFLICT. Всё — в одном согласованном изменении вместе с записью в outbox. - Публикация. Интеграционное событие
DealStageChangedEvent(топикpin.crm.deal-stage-changed.v1,AggregateId = DealId,AggregateType = "Deal") — через outbox. При переходе в группуCancelled— дополнительноDealCancelledEvent→pin.crm.deal-cancelled.v1. Отмена сделки переводит активную бронь→ Cancelledтем же согласованным изменением (INV-DEAL-8), поэтому release объекта вchessboardможет прийти двумя путями (deal-cancelledи каскадныйbooking-cancelled) — освобождение идемпотентно (hold-guardHoldDealId == DealId, INV-CHS-003): повторный release безопасен, объект «Свободен» ставится один раз; конкурентный workerbooking-expirationна уже отменённой брони — no-op (терминальный статус вне выборки). - Результат. Возвращается карточка сделки (
StepId,StepGroup,Status,UpdatedAt, свежийRowVersion); карточка занимает новую колонку канбана. Асинхронно:messengerрисует системную карточку «этап изменён» в чате сделки,notificationsуведомляет участников.
Диаграмма
Альтернативные Потоки
A1. Авто-переход при бронировании / продаже
- Создание брони (../domain/booking.md, UC-BKG-001)
авто-переводит
Application → Booking; оплата/подтверждение брони —StartSale(Booking → Sale, INV-DEAL-6); истечение брони воркеромbooking-expiration—Booking → Application; завершение застройщиком (../api/developer/complete-deal.md) —Sale → Completed. - Каждый переход публикует тот же
DealStageChangedEvent(ChangedByUserId = nullдля системных авто-переходов) и пишет строку таймлайна.
Сходится с основным потоком на шаге 6; drag-n-drop актором не требуется (карточка двигается автоматически).
A2. Настройка доски застройщиком
- Менеджер застройщика (
crm.pipeline.manage) создаёт/переименовывает/перекрашивает, меняет порядок и архивирует этапы: ../api/developer/create-step.md, update-step.md, reorder-steps.md, archive-step.md. - Системные группы
PipelineStepGroup(SEED) неизменны через API (INV-STEP-1/5); каждый этап обязан быть привязан к группе; при архиве этапа с активными сделками они переносятся в этап-приёмник той же группы (INV-STEP-4) —StepGroup/Statusсделок не меняются, событие наружу не публикуется.
Меняет только справочник этапов; напрямую в основной поток не входит (обеспечивает предусловие «доска настроена»).
A3. Ручной кросс-tenant перевод Менеджером ПИН
- Менеджер ПИН при конфликте/ошибке вручную двигает этап/статус любой сделки платформы (platform-scope, обход record-level) — ../api/pin-manager/move-deal-step.md, UC-CRM-006 (US-MGR-03).
- Публикуется тот же
DealStageChangedEvent(ChangedByUserId = менеджер ПИН); в таймлайн добавляется запись перехода (при арбитраже —ManualNote, INV-TLE-5).
Сходится с основным потоком на шаге 6.
Постусловие
DealEntity.StepId/StepGroup(иStatusпри смене группы) обновлены;UpdatedAt/UpdatedByUserIdпроставлены; при переходе вCancelled—CancelledAt/CancelReason, активная бронь отменена.- В таймлайне сделки — строка
StageChanged(+StatusChanged/DealCancelledпри смене статуса/отмене). - В outbox —
pin.crm.deal-stage-changed.v1(+pin.crm.deal-cancelled.v1при переходе вCancelled); no-op повтор событий не публикует. - Карточка в новой колонке канбана;
messengerиnotificationsдоведены eventual.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_FAILED | 400 | DealId/TargetStepId пусты или длина CancelReason вне 1..1000 | Исправить запрос |
CRM_DEAL_NOT_FOUND | 404 | Сделки нет или она вне scope актора (существование не раскрывается) | Перечитать доску |
CRM_DEAL_STEP_FORBIDDEN | 422 | Целевой этап чужого TenantId/архивный; группа Cancelled без CancelReason; группа Completed через drag-n-drop (INV-DEAL-5/7) | Выбрать корректный этап / указать причину |
CRM_DEAL_CONTRACT_TYPE_REQUIRED | 409 | Переход в группу статуса Sale без ContractType/PaymentMethod (INV-DEAL-6) | Указать тип договора и способ оплаты |
CRM_DEAL_ALREADY_TERMINAL | 409 | Сделка Completed/Cancelled (INV-DEAL-9) | Сделка завершена, действие недоступно |
CRM_DEAL_CONFLICT | 409 | Гонка drag-n-drop (оптимистичная блокировка) | Перечитать доску и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DealMovedToStep | Перевод сделки на этап (смена StepId) | Тот же обработчик: строка DealTimelineEventEntity(StageChanged) |
DealStatusChanged | Смена Status при переходе в другую группу | Тот же обработчик: строка таймлайна StatusChanged/DealCancelled; сборка DealStageChangedEvent |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DealStageChangedEvent | pin.crm.deal-stage-changed.v1 | Любое изменение StepId/Status (drag-n-drop, авто-переход, ручной перевод); кроме no-op | messenger (карточка «этап изменён»), notifications (уведомление участникам) — ../events/deal-stage-changed.md |
DealCancelledEvent | pin.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 без CancelReason → CRM_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 | Негативные тесты |
Обратные ссылки
Автоссылки: где используется этот документ.
- scenarios (2): UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: ../api/agent/move-deal-step.md; кросс-tenant — ../api/pin-manager/move-deal-step.md; настройка доски — ../api/developer/create-step.md, update-step.md, reorder-steps.md, archive-step.md, list-steps.md.
- Domain entity: ../domain/deal.md (перевод на этап, INV-DEAL-5..9, enum
DealStatus), ../domain/pipeline-step.md (этапы, enumPipelineStepGroup, INV-STEP-1..5), ../domain/deal-timeline-event.md (типStageChanged). - Kafka события: ../events/deal-stage-changed.md, ../events/deal-cancelled.md.
- Соседние use cases: UC-CRM-002 (создание сделки), UC-CRM-006 (ручное вмешательство Менеджера ПИН, US-MGR-03); US-реестр — user-stories-deal.md (US-AGT-01, US-DEV-01).
- ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first.