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

POST /api/pin-manager/crm/deals/{dealId}/override-status — Ручная смена статуса сделки

Назначение

Даёт Менеджеру ПИН вручную выставить бизнес-статус сделки, когда штатный lifecycle расходится с реальностью — конфликт сторон, сбой воркера/консюмера, ошибочный авто-переход (US-MGR-03). Это арбитражный инструмент: в отличие от канбан-перевода move-deal-step.md (двигает StepId, статус следует за группой), здесь задаётся целевой DealStatus, а этап синхронизируется под него. Доступны 4 статуса US-INV-03 — заявка/бронь/продажа/отказ (Application/Booking/Sale/Cancelled); Completed этим методом недостижим (завершение — только lifecycle застройщика после Росреестра, complete-deal.md, INV-DEAL-7). Арбитраж получает ограниченный обход guard-ов DealEntity (см. таблицу в шаге 4). Каждая смена фиксируется в таймлайне двумя строками — переход (StatusChanged либо DealCancelled) и обязательная ручная пометка-обоснование ManualNote (INV-TLE-5), участники уведомляются. Дом полей, статусы и инварианты — DealEntity.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areapin-manager
Feature groupdeal
Статусdraft
Документdocs/06-services/crm/api/pin-manager/override-deal-status.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: Status/StepId/StepGroup(/CancelledAt/CancelReason), UpdatedByUserId, версия записи; бизнес-история — строки таймлайна перехода + ManualNote (append-only, deal-timeline-event.md)
Доступ

Право crm.deals.manage действует в platform-scope — сквозной кросс-tenant доступ; TenantId целевой сделки берётся из самой сделки, а не из claims актора. Отсутствие права → 403 FORBIDDEN до бизнес-логики метода; record-level и tenant-фильтр для этого метода не применяются.

HTTP-контракт

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

POST … /override-status (а не PUT … /status): явная ручная команда-действие с обязательным обоснованием (Comment) и обходом guard-ов, а не REST-замена поля ресурса.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdroute {dealId}GuidДа!= Guid.Empty; существующая сделка (шаг 2)
TargetStatusbodyDealStatus (enum строкой)Да{Application, Booking, Sale, Cancelled}; Completed запрещён (шаг 2)
CommentbodystringДа1..2000 после trim, не пустое; текст ManualNote и (для Cancelled) CancelReasonне PII (свободный текст оператора); в логи не пишется
RowVersionbodyuint?Нетнаблюдаемая версия карточки для оптимистической блокировки (шаг 5)
ActorUserIdidentity claimsGuidДаиз JWT Менеджера ПИН; автор перехода и строк таймлайна

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

Та же проекция карточки канбана, что в move-deal-step.md (дом полей — DealEntity, EscrowAccountNumber и данные фиксации в карту не входят). Релевантная дельта операции:

ПолеТипИсточникОписание
DealIdGuidDealEntity.IdИдентификатор сделки.
Statusstring (enum DealStatus)DealEntity.Status (после override)Новый статус процесса.
StepIdGuidDealEntity.StepIdЭтап-приёмник целевой группы (шаг 4).
StepGroupstring (enum PipelineStepGroup)DealEntity.StepGroupСистемная группа этапа.
UpdatedAtDateTimeOffsetDealEntity.UpdatedAtМомент смены статуса.
RowVersionuintDealEntity.RowVersionТокен для следующей операции.

Алгоритм

  1. Контекст и доступ. Операцию выполняет Менеджер ПИН с permission crm.deals.manage в platform-scope; TenantId целевой сделки берётся из неё самой (шаг 3), а не из claims.

  2. Проверка входа и предусловий.

    • формат: DealId != Guid.Empty; TargetStatus ∈ {Application, Booking, Sale, Cancelled}; Comment не пустой после trim, 1..2000 → иначе VALIDATION_FAILED;
    • Completed как цель: отклоняется (вне множества) — завершение делает только застройщик после Росреестра (complete-deal.md, INV-DEAL-7), это не разрешение статус-конфликта; на уровне бизнес-правил дублируется как CRM_DEAL_STATUS_OVERRIDE_FORBIDDEN (422);
    • существование сделки: сделки с таким DealId нет → CRM_DEAL_NOT_FOUND (404);
    • допустимость состояния (терминальность): оживление уже завершённой сделки (Status == Completed) запрещено даже арбитражу — необратимые downstream-эффекты investor-owner/rewards/chessboard, компенсирующего контракта нет → CRM_DEAL_ALREADY_TERMINAL (409). Источник Cancelled разрешён (оживление — цель US-MGR-03).
  3. Что читаем. Сделку по DealId сквозным доступом (без tenant/record-level предиката); нужен её TenantId для резолва этапа. Если TargetStatus уже совпадает со статусом сделки → no-op (шаг 4). Иначе резолв этапа-приёмника целевой группы: targetGroup = mapStatusToGroup(TargetStatus) (Application→Application, Booking→Booking, Sale→Contract (входная группа продажи), Cancelled→Cancelledpipeline-step.md); берётся живой этап этой группы у застройщика (дефолтный первым). Нет живого этапа группы → CRM_DEAL_STEP_NOT_FOUND (422, конфиг воронки застройщика).

  4. Бизнес-правила и переходы. No-op (статус уже целевой): вернуть текущую карточку, без мутаций/событий/ строк таймлайна. Иначе — переход с actor-ролью PinManager и ограниченным арбитражным обходом guard-ов:

    GuardОбход арбитражемПоведение override
    INV-DEAL-9 терминальностьДа (только из Cancelled)Оживление отменённой сделки в Application/Booking/Sale; из Completed — запрещено (шаг 2)
    INV-DEAL-7/8 порядок стадийДаРазрешён откат назад (Sale→Booking, Sale→Application) и любой прямой скачок
    INV-DEAL-6 Sale требует ContractType/PaymentMethodНетЦель Sale без обоих полей → CRM_DEAL_CONTRACT_TYPE_REQUIRED (409): целостность договора не обходится
    • Цель Cancelled: Cancel(reason = Comment, actor = PinManager) (INV-DEAL-8) — Status ← Cancelled, CancelledAt ← now, CancelReason ← Comment, активная бронь отменяется в той же записи (объект освобождается консюмером); переход на дефолтный этап группы Cancelled. Строка таймлайна DealCancelled (TimelineCancelledPayload { Reason = Comment }).
    • Цель Application/Booking/Sale: OverrideStatus(TargetStatus, step, actor = PinManager) — денормализация StepId ← step.Id, StepGroup ← step.Group, Status ← TargetStatus; guard INV-DEAL-6 для Sale. Строка таймлайна StatusChanged (TimelineStatusChangedPayload { FromStatus, ToStatus, Reason = Comment }).
    • маппинг (обе ветви): UpdatedByUserId ← ActorUserId, UpdatedAt ← now, версия записи +1. Дополнительно строка ManualNote (TimelineManualNotePayload { Text = Comment }, INV-TLE-5: создаёт только Менеджер ПИН / Служба заботы) фиксирует ручной характер (US-MGR-03). Обе строки — в той же согласованной записи (INV-TLE-2), ActorRole = PinManager.
  5. Что меняется. Сделка (статус/этап/группа, для Cancelled — отмена брони, автор, версия) и две append-only строки таймлайна изменяются в одной согласованной записи. Оптимистическая блокировка: параллельное изменение сделки (или устаревший переданный RowVersion) → CRM_DEAL_CONFLICT (409); клиент перечитывает карточку.

  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. При цели Cancelledдополнительно DealCancelledEvent → topic pin.crm.deal-cancelled.v1, { DealId, TenantId, PropertyObjectId, CancelledByUserId = ActorUserId, CancelReason = Comment, CancelledAt } (deal-cancelled.md) — освобождение объекта консюмером chessboard. DealCompletedEvent этим методом не публикуется (Completed недостижим, deal-completed.md). Строки таймлайна собственных Kafka-событий не порождают. No-op событий не публикует.

  7. Результат. Проекция карточки после override (Status, StepId, StepGroup, UpdatedAt, свежий RowVersion) → HTTP 200.

Важно

Оживление из Cancelled восстанавливает только процессную сделку (Status/StepId/StepGroup) — объект недвижимости в «Резерв/Бронь» автоматически не возвращается: chessboard владеет статусами объекта, при необходимости бронь пересоздаётся отдельно (см. AC-4).

Диаграмма

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

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

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId = Guid.Empty; TargetStatus вне множества / Completed; Comment пустой или > 2000«Проверьте статус и комментарий»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission crm.deals.manage (platform-scope)«Доступ запрещён»no retry
CRM_DEAL_NOT_FOUND404сделки с DealId нет«Сделка не найдена»no retry
CRM_DEAL_STATUS_OVERRIDE_FORBIDDEN422целевой статус Completed (завершение — lifecycle застройщика, INV-DEAL-7)«Завершение сделки выполняет застройщик»no retry
CRM_DEAL_STEP_NOT_FOUND422у застройщика нет живого этапа целевой группы (конфиг воронки)«Нет этапа для целевого статуса»no retry
CRM_DEAL_CONTRACT_TYPE_REQUIRED409цель Sale без ContractType/PaymentMethod (INV-DEAL-6, не обходится)«Укажите тип договора и способ оплаты»no retry
CRM_DEAL_ALREADY_TERMINAL409сделка в Completed (оживление необратимо, INV-DEAL-9)«Сделка завершена и не редактируется»no retry
CRM_DEAL_CONFLICT409конкурентное изменение сделки (устаревший RowVersion)«Сделка изменена, обновите данные»retry (перечитать карточку)

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

  • Контракт: route + TargetStatus/Comment/RowVersion (body) → DealCardModel. Новые optional-поля карточки — additive; смена маршрута/семантики статуса — breaking (migration + rollout). Новые значения DealStatus/PipelineStepGroup — additive (см. deal-stage-changed.md).
  • Logs: pin-manager deal-override-status requested/overridden/noop/conflict (DealId, TenantId, FromStatusToStatus, ActorUserId; ФИО/телефон и текст Comment в логи не пишутся); metrics: crm_pin_manager_operations_total{operation="override-status",result}, latency p95 < 500 мс (SLO команд); traces — сквозной trace через traceparent; dashboard pin-crm.

Постусловия

После успешного (не no-op) вызова гарантированно: Status сделки = TargetStatus; StepId/StepGroup синхронизированы с целевой группой; UpdatedByUserId = Менеджер ПИН, версия карточки +1; в таймлайн добавлены две append-only строки — переход (StatusChanged/DealCancelled) и ManualNote с обоснованием; опубликовано DealStageChangedEvent (при цели Cancelled — дополнительно DealCancelledEvent). При no-op состояние сделки, таймлайн и шина событий не изменяются. Промежуточное состояние (eventual consistency): клиент получает 200 с новой карточкой сразу; участники увидят карточку «статус изменён» в чате и push после обработки deal-stage-changed консюмерами messenger/notifications; при Cancelled объект недвижимости освобождается в шахматке только после обработки deal-cancelled консюмером chessboard — в окне между commit и этой обработкой объект ещё числится занятым.

Acceptance Criteria

  • AC-1 (happy — прямой перевод). Дано: сделка в Application, у застройщика есть живой этап группы Booking. Когда: Менеджер ПИН вызывает override на Booking с комментарием. Тогда: Status = Booking, этап — приёмник группы Booking, в таймлайне строки StatusChanged + ManualNote, публикуется DealStageChangedEvent, ответ 200 с новым RowVersion.
  • AC-2 (no-op). Дано: сделка уже в Sale. Когда: override на Sale. Тогда: мутаций, строк таймлайна и событий нет; возвращается текущая карточка 200.
  • AC-3 (Completed запрещён). Дано: любая сделка. Когда: TargetStatus = Completed. Тогда: отказ (VALIDATION_FAILED / CRM_DEAL_STATUS_OVERRIDE_FORBIDDEN), состояние без изменений.
  • AC-4 (оживление из Cancelled). Дано: сделка в Cancelled. Когда: override на Application. Тогда: Status = Application, строки StatusChanged + ManualNote; объект недвижимости автоматически в «Резерв/Бронь» не возвращается (при необходимости бронь пересоздаётся).
  • AC-5 (Sale без реквизитов договора). Дано: сделка без ContractType/PaymentMethod. Когда: override на Sale. Тогда: отказ CRM_DEAL_CONTRACT_TYPE_REQUIRED (INV-DEAL-6 не обходится), без изменений.
  • AC-6 (терминальный Completed). Дано: сделка в Completed. Когда: override на любой статус. Тогда: отказ CRM_DEAL_ALREADY_TERMINAL, без изменений.
  • AC-7 (конкурентное изменение). Дано: переданный RowVersion устарел (сделку изменил другой процесс). Когда: override. Тогда: отказ CRM_DEAL_CONFLICT, без изменений; клиент перечитывает карточку.
  • AC-8 (цель Cancelled с активной бронью). Дано: сделка в Booking с активной бронью. Когда: override на Cancelled. Тогда: Status = Cancelled, CancelReason = комментарий, публикуются DealStageChangedEvent и DealCancelledEvent; объект освобождается консюмером chessboard в окне eventual-consistency.

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

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

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