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

POST /api/pin-manager/crm/deals/{dealId}/move-step — Ручной перевод по этапу (Менеджер ПИН)

Назначение

Даёт Менеджеру ПИН вручную подтвердить/перевести сделку на этап канбана застройщика, когда штатный авто-переход недоступен (сбой воркера/консюмера, арбитраж, ручное подтверждение стадии — US-MGR-05). Перевод — тот же доменный переход MoveToStep (INV-DEAL-5), что и на канбане агента, но со сквозным (кросс-tenant) platform-scope доступом Менеджера ПИН (US-MGR-01: авто-участник всех сделок). Меняет текущий этап и, при смене системной группы, синхронизирует статус процесса; каждый перевод фиксируется в таймлайне двумя строками — StageChanged (сам переход) и ManualNote (пометка о ручном характере, INV-TLE-5). Терминальные группы Completed/Cancelled через этот метод недостижимы — завершение и отмена выполняются отдельными lifecycle-операциями. Дом полей, статусы и инварианты — DealEntity.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areapin-manager
Feature groupdeal
Статусdraft
Документdocs/06-services/crm/api/pin-manager/move-deal-step.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — Менеджер ПИН (оператор платформы)
Auth policy / Permissionscrm.deals.manage в platform-scope (сквозной доступ ко всем тенантам); гейт области pin-manager
Scope (RBAC)платформа (кросс-tenant): Менеджер ПИН видит и ведёт любую сделку (US-MGR-01)
Record-levelне применяется — обход record-level и tenant-фильтра (platform-scope)
Tenant isolationСделка TenantId = DeveloperCompanyId (владелец доски — застройщик, ADR-0052). Операция сквозная; целевой TenantId берётся из самой сделки, а не из claims актора
Auditupdate: StepId/StepGroup(/Status), UpdatedByUserId, версия записи; бизнес-история — строки таймлайна StageChanged + ManualNote (append-only, deal-timeline-event.md)
Доступ

Permission crm.deals.manage в platform-scope даёт Менеджеру ПИН сквозной кросс-tenant доступ: record-level и tenant-фильтр не применяются, целевой TenantId определяется из самой сделки, а не из claims актора (ADR-0052, US-MGR-01). В отличие от агентского аналога move-deal-step.md (record-level по OriginAgencyCompanyId), здесь доступна любая сделка любого застройщика.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/pin-manager/crm/deals/{dealId}/move-step
Success status200 (обновлённая карточка сделки)
Idempotency headerне применяется — повтор на тот же этап = no-op (шаг 4); гонка ловится сравнением версии карточки
Correlationсквозной trace через заголовок traceparent

POST … /move-step (а не PUT … /step, как на канбане агента): это явная ручная команда-действие Менеджера ПИН с пометкой-обоснованием (Note), а не REST-замена ресурса.

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty; существующая сделка (шаг 2)
TargetStepIdbodyGuidДа!= Guid.Empty; живой этап TenantId сделки, нетерминальная группа (шаг 2, INV-DEAL-5)
Notebodystring?Нет0..2000 после trim; текст ручной пометки ManualNote (шаг 4)не PII (свободный текст оператора)
RowVersionbodyuint?Нетнаблюдаемая версия карточки для оптимистической блокировки (шаг 5)
ActorUserIdidentity claimsGuidДаиз JWT Менеджера ПИН; фиксируется как автор перехода и строк таймлайна

Модель ответа DealCardModel

Проекция карточки канбана; дом полей — DealEntity. EscrowAccountNumber (Sensitive) и данные фиксации в карту не входят.

ПолеТипОбязательностьИсточникMaskingОписание
DealIdGuidДаDealEntity.IdИдентификатор сделки.
TitlestringДаDealEntity.TitleЗаголовок карточки.
StepIdGuidДаDealEntity.StepId (после перехода)Текущий этап канбана.
StepGroupstring (enum PipelineStepGroup)ДаDealEntity.StepGroupСистемная группа этапа.
Statusstring (enum DealStatus)ДаDealEntity.StatusСтатус процесса после перехода.
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdОбъект сделки (chessboard).
ProjectIdGuidДаDealEntity.ProjectIdПроект объекта.
PriceMoney { Amount: decimal, Currency: string }ДаDealEntity.PriceСтоимость объекта недвижимости (snapshot).
PrimaryInvestorUserIdGuidДаDealEntity.PrimaryInvestorUserIdОсновной инвестор сделки (возможны соинвесторы InvestorSecondary).
ExecutorAgentUserIdGuidДаDealEntity.ExecutorAgentUserIdТекущий агент-исполнитель сделки (получатель комиссии).
ChatStatusstring (enum DealChatStatus)ДаDealEntity.ChatStatus (проекция)Готовность чата сделки.
UpdatedAtDateTimeOffsetДаDealEntity.UpdatedAtМомент перехода.
RowVersionuintДаDealEntity.RowVersionТокен для следующей операции.

Алгоритм

  1. Контекст и доступ. Операцию выполняет Менеджер ПИН с permission crm.deals.manage в platform-scope; TenantId целевой сделки берётся из неё самой (шаг 3), а не из claims актора.
  2. Проверка входа и предусловий.
    • формат: DealId != Guid.Empty, TargetStepId != Guid.Empty, длина Note 0..2000 (если задан) → иначе VALIDATION_FAILED;
    • существование сделки: сделки с таким DealId нет → CRM_DEAL_NOT_FOUND (404);
    • существование целевого этапа: этап должен быть живым этапом того же застройщика (step.TenantId == deal.TenantId, не архивный) — иначе CRM_DEAL_STEP_FORBIDDEN (422, INV-DEAL-5);
    • допустимость целевой группы: группа этапа не может быть терминальной Completed/Cancelled — завершение (INV-DEAL-7) и отмена (INV-DEAL-8) выполняются отдельными lifecycle-операциями, которые публикуют свои события (deal-completed/deal-cancelled); иначе — CRM_DEAL_STEP_FORBIDDEN (422);
    • допустимость состояния: сделка не в терминальном статусе (Status ∉ {Completed, Cancelled}, INV-DEAL-9) — иначе CRM_DEAL_ALREADY_TERMINAL (409).
  3. Что читаем. Сделку по DealId сквозным доступом (без tenant/record-level предиката — сквозной доступ Менеджера ПИН, US-MGR-01); нужен её TenantId для проверки этапа. Затем этап-приёмник по TargetStepId.
  4. Бизнес-правила и переходы. Если TargetStepId == deal.StepIdno-op: вернуть текущую карточку, мутаций/событий/строк таймлайна нет. Иначе переход MoveToStep(step, actor = PinManager) (INV-DEAL-5):
    • guard: step.TenantId == deal.TenantId, этап не архивный (доменный отказ → CRM_DEAL_STEP_FORBIDDEN); денормализация StepId ← step.Id, StepGroup ← step.Group;
    • в пределах группы (step.Group == deal.StepGroup): Status не меняется;
    • смена группы: Status синхронизируется по маппингу групп (pipeline-step.md): Application→Application, Booking→Booking, Contract/Payment/RegistrationSale. Группы статуса Sale требуют ContractType/PaymentMethod (INV-DEAL-6) → иначе CRM_DEAL_CONTRACT_TYPE_REQUIRED;
    • маппинг: UpdatedByUserId ← ActorUserId, UpdatedAt ← now, версия записи +1. В таймлайн добавляются две строки в той же согласованной записи (INV-TLE-2): StageChanged (TimelineStageChangedPayload { FromStepId, FromStepName, ToStepId, ToStepName, ToStepGroup }) и ManualNote (TimelineManualNotePayload { Text }, INV-TLE-5: создаёт только Менеджер ПИН/Служба заботы) — Text = Note?.Trim(), иначе канонический маркер «Этап переведён вручную (Менеджер ПИН)» (US-MGR-05/06); ActorRole = PinManager.
  5. Что меняется. Сделка (этап/группа/статус, автор, версия) и две append-only строки таймлайна изменяются в одной согласованной записи. Оптимистическая блокировка: если сделку параллельно изменил другой участник (или переданный RowVersion устарел) → CRM_DEAL_CONFLICT (409); клиент перечитывает карточку. No-op повтор (шаг 4) ничего не меняет.
  6. Событие. При реальном переходе публикуется интеграционное DealStageChangedEvent → topic pin.crm.deal-stage-changed.v1 (ContractVersion = 1, AggregateType = "Deal"), payload { DealId, TenantId, FromStepId, ToStepId, FromStepGroup?, ToStepGroup, FromStatus, ToStatus, ChangedByUserId = ActorUserId, ChangedAt = UpdatedAt } — контракт deal-stage-changed.md. Строки таймлайна собственных Kafka-событий не порождают. No-op событий не публикует.
  7. Результат. Проекция карточки после перехода (StepId, StepGroup, Status, UpdatedAt, свежий RowVersion) → HTTP 200.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияDealStageChangedEventpin.crm.deal-stage-changed.v1 (всегда, кроме no-op)
Внешние вызовынет (этап-приёмник читается из локального справочника этапов)
Проекции / поискпоисковая проекция сделки обновляется по StepGroup/StepId; сделка перемещается между колонками канбана
Уведомленияопосредованно: notifications — консюмер deal-stage-changed (уведомление участникам); messenger — системная карточка «этап изменён» в чате сделки

Ошибки и failure modes

Ошибки

Код CRM_DEAL_STEP_FORBIDDEN (422) объединяет два условия: целевой этап чужого TenantId или архивный, и целевая группа терминальная (Completed/Cancelled) — обе недостижимы через этот метод, только через отдельные lifecycle-операции (INV-DEAL-5/7/8). Терминальная сделка (Status ∈ {Completed, Cancelled}) не редактируется независимо от целевого этапа — 409 CRM_DEAL_ALREADY_TERMINAL (INV-DEAL-9). Конкурентное изменение карточки (устаревший RowVersion) — 409 CRM_DEAL_CONFLICT, ретрай после перечитки карточки.

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId/TargetStepId = Guid.Empty или длина Note > 2000«Проверьте корректность запроса»no retry
CRM_DEAL_NOT_FOUND404сделки с DealId нет«Сделка не найдена»no retry
CRM_DEAL_STEP_FORBIDDEN422целевой этап чужого TenantId/архивный; целевая группа терминальная Completed/Cancelled (INV-DEAL-5/7/8)«Недопустимый целевой этап»no retry
CRM_DEAL_CONTRACT_TYPE_REQUIRED409переход в группу статуса Sale без ContractType/PaymentMethod (INV-DEAL-6)«Укажите тип договора и способ оплаты»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка в терминальном Completed/Cancelled (INV-DEAL-9)«Сделка завершена и не редактируется»no retry
CRM_DEAL_CONFLICT409конкурентное изменение сделки (устаревший RowVersion)«Сделка изменена, обновите данные»retry (перечитать карточку)

Предусловия и постусловия

Предусловия (истинны до сценария):

  • актор — Менеджер ПИН с permission crm.deals.manage в platform-scope; кросс-tenant, целевой TenantId берётся из самой сделки;
  • сделка нетерминальна (Status ∉ {Completed, Cancelled}, INV-DEAL-9);
  • целевой этап — живой этап того же застройщика (step.TenantId == deal.TenantId, не архивный), нетерминальная группа (не Completed/Cancelled, INV-DEAL-5).

Постусловия (гарантированы после реального перехода):

  • StepId/StepGroup обновлены; при смене группы Status синхронизирован по маппингу групп (Application/Booking/Sale); UpdatedByUserId/UpdatedAt, версия записи +1;
  • в ленту добавлены две append-only строки — StageChanged и ManualNote (INV-TLE-5) — в одном согласованном изменении;
  • опубликовано интеграционное DealStageChangedEvent (топик pin.crm.deal-stage-changed.v1) — атомарно с мутацией;
  • No-op (TargetStepId == deal.StepId): состояние, события и таймлайн не меняются.

Интеграционная последовательность (после коммита)

Синхронно в запросе: проверки (INV-DEAL-5..9) → MoveToStep(actor = PinManager) → сохранение сделки + двух строк таймлайна + фиксация исходящего события → 200 клиенту (карточка с новым StepId/Status/RowVersion; сделка сразу в новой колонке канбана, обе строки — в «Ходе сделки»). Далее — асинхронно, событие доставляется гарантированно (at-least-once), окно eventual-consistency — секунды:

  1. DealStageChangedEventpin.crm.deal-stage-changed.v1: messenger — системная карточка «этап изменён» в чате сделки; notifications — уведомление участникам; поисковая проекция сделки обновляется по StepGroup/StepId.
Eventual consistency

До доставки участники уже видят обновлённую карточку сделки (source of truth — crm), а карточка в чате/уведомление/поисковый индекс догоняют с задержкой.

Что предоставляет: DealStageChangedEvent { DealId, TenantId, FromStepId, ToStepId, FromStepGroup?, ToStepGroup, FromStatus, ToStatus, ChangedByUserId, ChangedAt }, ContractVersion = 1, AggregateType = "Deal", ключ партиционирования — DealId (порядок переходов в пределах сделки сохраняется); no-op события не публикует. Что ожидает: справочник этапов pipeline_steps содержит целевой этап того же tenant (не архивный) — читается синхронно из своей схемы, внешних вызовов нет. Конкурентность/компенсации: оптимистическая блокировка по RowVersion — при устаревшей версии → CRM_DEAL_CONFLICT, проигравший перечитывает и повторяет; компенсаций/саги нет — переход, две строки таймлайна и событие фиксируются атомарно на одном агрегате.

Acceptance Criteria

  • Happy path в пределах группы. Дано: сделка нетерминальна, целевой этап той же группы того же застройщика. Когда: POST …/move-step {targetStepId}. Тогда: 200, StepId сменился, Status без изменений, добавлены строки StageChanged + ManualNote, опубликовано DealStageChangedEvent.
  • Смена группы. Дано: переход из группы Booking в Contract, у сделки заданы ContractType/PaymentMethod. Когда: POST. Тогда: 200, StepGroup = Contract, Status: Booking → Sale.
  • Смена в Sale без данных договора. Дано: переход в группу статуса Sale, ContractType/PaymentMethod не заданы. Когда: POST. Тогда: 409 CRM_DEAL_CONTRACT_TYPE_REQUIRED (INV-DEAL-6).
  • No-op. Дано: TargetStepId == deal.StepId. Когда: POST. Тогда: 200 c текущей карточкой, без мутаций/события/строк таймлайна.
  • Терминальная целевая группа. Дано: целевой этап группы Completed/Cancelled. Когда: POST. Тогда: 422 CRM_DEAL_STEP_FORBIDDEN (завершение/отмена — отдельные lifecycle-операции).
  • Чужой/архивный этап. Дано: step.TenantId != deal.TenantId или этап архивный. Когда: POST. Тогда: 422 CRM_DEAL_STEP_FORBIDDEN.
  • Терминальная сделка. Дано: Status ∈ {Completed, Cancelled}. Когда: POST. Тогда: 409 CRM_DEAL_ALREADY_TERMINAL (INV-DEAL-9).
  • Конкурентное изменение. Дано: сделку параллельно изменили (устаревший RowVersion). Когда: POST. Тогда: 409 CRM_DEAL_CONFLICT, клиент перечитывает карточку.
  • Кастомная/каноническая пометка. Дано: Note задан. Тогда: строка ManualNote содержит Note.Trim(); если Note пуст — канонический маркер «Этап переведён вручную (Менеджер ПИН)».
  • Невалидный вход. Когда: DealId/TargetStepId = Guid.Empty или Note > 2000. Тогда: 400 VALIDATION_FAILED.

Совместимость и наблюдаемость

  • Контракт: route + TargetStepId/Note/RowVersion (body) → DealCardModel. Новые optional-поля карточки — additive; смена маршрута/семантики статуса — breaking (migration + rollout). Новые значения PipelineStepGroup/DealStatus — additive (см. deal-stage-changed.md).
  • Logs: pin-manager deal-move-step requested/moved/noop/conflict (DealId, TenantId, FromStepIdToStepId, ActorUserId; ФИО/телефон и текст Note в логи не пишутся); metrics: crm_pin_manager_deal_move_step_total{result}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm.

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

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

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