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 area | pin-manager |
| Feature group | deal |
| Статус | draft |
| Документ | docs/06-services/crm/api/pin-manager/override-deal-status.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — Менеджер ПИН (оператор платформы) |
| Auth policy / Permissions | crm.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 |
| Audit | update: 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-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/pin-manager/crm/deals/{dealId}/override-status |
| Success status | 200 (обновлённая карточка сделки) |
| Idempotency header | не применяется — повтор на текущий статус = no-op (шаг 4); гонка ловится сравнением версии карточки |
| Correlation | сквозной trace через заголовок traceparent |
POST … /override-status(а неPUT … /status): явная ручная команда-действие с обязательным обоснованием (Comment) и обходом guard-ов, а не REST-замена поля ресурса.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
DealId | route {dealId} | Guid | Да | != Guid.Empty; существующая сделка (шаг 2) | — |
TargetStatus | body | DealStatus (enum строкой) | Да | ∈ {Application, Booking, Sale, Cancelled}; Completed запрещён (шаг 2) | — |
Comment | body | string | Да | 1..2000 после trim, не пустое; текст ManualNote и (для Cancelled) CancelReason | не PII (свободный текст оператора); в логи не пишется |
RowVersion | body | uint? | Нет | наблюдаемая версия карточки для оптимистической блокировки (шаг 5) | — |
ActorUserId | identity claims | Guid | Да | из JWT Менеджера ПИН; автор перехода и строк таймлайна | — |
Модель ответа DealCardModel
Та же проекция карточки канбана, что в move-deal-step.md (дом полей —
DealEntity, EscrowAccountNumber и данные фиксации в карту не входят).
Релевантная дельта операции:
| Поле | Тип | Источник | Описание |
|---|---|---|---|
DealId | Guid | DealEntity.Id | Идентификатор сделки. |
Status | string (enum DealStatus) | DealEntity.Status (после override) | Новый статус процесса. |
StepId | Guid | DealEntity.StepId | Этап-приёмник целевой группы (шаг 4). |
StepGroup | string (enum PipelineStepGroup) | DealEntity.StepGroup | Системная группа этапа. |
UpdatedAt | DateTimeOffset | DealEntity.UpdatedAt | Момент смены статуса. |
RowVersion | uint | DealEntity.RowVersion | Токен для следующей операции. |
Алгоритм
-
Контекст и доступ. Операцию выполняет Менеджер ПИН с permission
crm.deals.manageв platform-scope;TenantIdцелевой сделки берётся из неё самой (шаг 3), а не из claims. -
Проверка входа и предусловий.
- формат:
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).
- формат:
-
Что читаем. Сделку по
DealIdсквозным доступом (без tenant/record-level предиката); нужен еёTenantIdдля резолва этапа. ЕслиTargetStatusуже совпадает со статусом сделки → no-op (шаг 4). Иначе резолв этапа-приёмника целевой группы:targetGroup = mapStatusToGroup(TargetStatus)(Application→Application,Booking→Booking,Sale→Contract(входная группа продажи),Cancelled→Cancelled— pipeline-step.md); берётся живой этап этой группы у застройщика (дефолтный первым). Нет живого этапа группы →CRM_DEAL_STEP_NOT_FOUND(422, конфиг воронки застройщика). -
Бизнес-правила и переходы. 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.
- Цель
-
Что меняется. Сделка (статус/этап/группа, для
Cancelled— отмена брони, автор, версия) и две append-only строки таймлайна изменяются в одной согласованной записи. Оптимистическая блокировка: параллельное изменение сделки (или устаревший переданныйRowVersion) →CRM_DEAL_CONFLICT(409); клиент перечитывает карточку. -
События. При реальном переходе публикуется интеграционное
DealStageChangedEvent→ topicpin.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→ topicpin.crm.deal-cancelled.v1,{ DealId, TenantId, PropertyObjectId, CancelledByUserId = ActorUserId, CancelReason = Comment, CancelledAt }(deal-cancelled.md) — освобождение объекта консюмеромchessboard.DealCompletedEventэтим методом не публикуется (Completedнедостижим, deal-completed.md). Строки таймлайна собственных Kafka-событий не порождают. No-op событий не публикует. -
Результат. Проекция карточки после override (
Status,StepId,StepGroup,UpdatedAt, свежийRowVersion) → HTTP200.
Оживление из Cancelled восстанавливает только процессную сделку (Status/StepId/StepGroup) — объект
недвижимости в «Резерв/Бронь» автоматически не возвращается: chessboard владеет статусами объекта, при
необходимости бронь пересоздаётся отдельно (см. AC-4).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | DealStageChangedEvent → pin.crm.deal-stage-changed.v1 (всегда, кроме no-op); при цели Cancelled — дополнительно DealCancelledEvent → pin.crm.deal-cancelled.v1 |
| Внешние вызовы | нет (этап-приёмник читается из локального справочника этапов) |
| Проекции / поиск | поисковая проекция сделки обновляется по Status/StepGroup; сделка перемещается между колонками канбана. Оживление из Cancelled восстанавливает процессную сделку, но объект в «Резерв/Бронь» автоматически не возвращает — chessboard владеет статусами объекта, при необходимости бронь пересоздаётся |
| Уведомления | опосредованно: notifications — консюмер deal-stage-changed (+deal-cancelled) уведомляет участников; messenger — системная карточка «статус изменён» в чате сделки |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | DealId = Guid.Empty; TargetStatus вне множества / Completed; Comment пустой или > 2000 | «Проверьте статус и комментарий» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | нет permission crm.deals.manage (platform-scope) | «Доступ запрещён» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделки с DealId нет | «Сделка не найдена» | no retry |
CRM_DEAL_STATUS_OVERRIDE_FORBIDDEN | 422 | целевой статус Completed (завершение — lifecycle застройщика, INV-DEAL-7) | «Завершение сделки выполняет застройщик» | no retry |
CRM_DEAL_STEP_NOT_FOUND | 422 | у застройщика нет живого этапа целевой группы (конфиг воронки) | «Нет этапа для целевого статуса» | no retry |
CRM_DEAL_CONTRACT_TYPE_REQUIRED | 409 | цель Sale без ContractType/PaymentMethod (INV-DEAL-6, не обходится) | «Укажите тип договора и способ оплаты» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка в Completed (оживление необратимо, INV-DEAL-9) | «Сделка завершена и не редактируется» | no retry |
CRM_DEAL_CONFLICT | 409 | конкурентное изменение сделки (устаревший 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,FromStatus→ToStatus,ActorUserId; ФИО/телефон и текстCommentв логи не пишутся); metrics:crm_pin_manager_operations_total{operation="override-status",result}, latency p95 < 500 мс (SLO команд); traces — сквозной trace черезtraceparent; dashboardpin-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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CRM-006. Вмешательство Менеджера ПИН, UC-SUP-004. Ручное вмешательство через permissions
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности (дом полей, INV-DEAL-6..9, переходы
Cancel/OverrideStatus): deal.md; группы и маппинг наDealStatus, дефолтный этап группы: pipeline-step.md; таймлайнStatusChanged/DealCancelled/ManualNote(INV-TLE-5): deal-timeline-event.md. - События: deal-stage-changed.md,
deal-cancelled.md,
deal-completed.md (почему
Completedвне метода). - Соседние операции области: канбан-перевод move-deal-step.md, ручная запись add-timeline-note.md, завершение застройщиком complete-deal.md.
- Карта области
pin-managerи событий: README.md. Permission (дом): identity/domain/permission.md.