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

POST /api/developer/crm/pipeline-steps/{stepId}/archive — Архивировать этап

«Удаление» настраиваемого этапа воронки застройщика — это архив (soft-delete), а не физическое удаление: завершённые сделки истории ссылаются на этап (Deal.StepId, FK restrict — pipeline-step.md, deal.md), поэтому строка сохраняется (DeletedAt проставляется). Операция закрывает два инварианта этапа: на группу должен остаться живой этап (INV-STEP-3) и активные сделки архивируемого этапа переносятся в этап-приёмник той же группы в одном изменении (INV-STEP-4). Дом полей этапа — pipeline-step.md.

Назначение

Принимает от менеджера застройщика {StepId, TargetStepId?} и архивирует настраиваемый этап: помечает DeletedAt, при наличии активных сделок на этапе — переносит их в указанный этап-приёмник той же системной группы (массово, одним изменением). Тела ответа нет.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper (JWT менеджера застройщика)
Feature grouppipeline
Статусdraft
Документdocs/06-services/crm/api/developer/archive-step.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope)
Auth policy / Permissionscrm.pipeline.manage (менеджеры застройщика правят состав/порядок этапов)
Scope (RBAC)организация (застройщик): record-level по PipelineStepEntity.TenantId
Record-levelstep.TenantId = actor.TenantId (свой застройщик) либо Менеджер ПИН (platform-scope)
Tenant isolationTenantId в запросе = step.TenantId = DeveloperCompanyId; кросс-tenant запрещён (кроме platform-scope)
Auditdelete (DeletedByUserId/UpdatedByUserId = actor.UserId); переносимые сделки — UpdatedByUserId. PII отсутствует
Доступ

Архивировать этап может только менеджер застройщика — владелец tenant этапа (step.TenantId = actor.TenantId) — либо Менеджер ПИН (Operator, platform-scope, сквозной доступ). Кросс-tenant доступ запрещён (CRM_STEP_FORBIDDEN).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/pipeline-steps/{stepId}/archive
Success status204 (No Content — этап заархивирован, тела нет)
Idempotency headerне применяется (естественная защита: повтор на уже архивированном этапе → CRM_STEP_NOT_FOUND, soft-delete-фильтр исключает архивные)
Correlationсквозной trace через заголовок traceparent

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

Важно

TargetStepId обязателен, только если на архивируемом этапе есть активные сделки (INV-STEP-4) — иначе CRM_STEP_HAS_DEALS_TARGET_REQUIRED. Приёмник должен быть живым этапом той же группы (Group), что и архивируемый этап, — иначе CRM_STEP_TARGET_INVALID.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
StepIdrouteGuidДа!= Guid.Empty; существующий живой этап TenantId = актор (шаг 2)
TargetStepIdbodyGuid?Нет (обязателен при активных сделках, INV-STEP-4)!= Guid.Empty; != StepId; живой этап той же Group, TenantId = актор (шаг 2–3)
TenantIdidentity claimsGuidДаRequired; = step.TenantId (DeveloperCompanyId)Tenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера застройщика; permission crm.pipeline.manage

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

Отсутствует: по успеху HTTP 204 No Content. Обновлённая доска запрашивается отдельным list-методом области developer. Тело/PII не возвращаются.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.pipeline.manage) либо менеджер ПИН (platform-scope); tenant — TenantId = DeveloperCompanyId. Идемпотентность не требуется — повтор на архивированном этапе отсекается soft-delete-фильтром (шаг 2) и версией строки.
  2. Проверка входа и предусловий.
    • StepId != Guid.Empty; при заданном TargetStepId!= Guid.Empty и != StepId (перенос в себя запрещён) — иначе VALIDATION_FAILED;
    • этап существует среди живых (архивные скрыты soft-delete-фильтром) — иначе CRM_STEP_NOT_FOUND;
    • scope (record-level): step.TenantId == TenantId — иначе CRM_STEP_FORBIDDEN;
    • INV-STEP-3 (минимум один живой этап на группу): живых этапов группы должно остаться больше одного — иначе CRM_STEP_LAST_IN_GROUP;
    • INV-STEP-4 (активные сделки требуют приёмник): если на этапе есть активные сделки (Status ∉ {Completed, Cancelled}) и TargetStepId == nullCRM_STEP_HAS_DEALS_TARGET_REQUIRED;
    • валидность приёмника (если задан): он живой, того же tenant и той же Group, что архивируемый этап — иначе CRM_STEP_TARGET_INVALID.
  3. Что читаем. Сам этап (для мутации) и счётчики: число живых этапов группы (INV-STEP-3) и число активных сделок этапа (INV-STEP-4) — без материализации строк. При активных сделках приёмник уже провалидирован. Терминальные сделки (Completed/Cancelled) на этапе не переносятся — они остаются ссылаться на архивированный этап как история (soft-delete сохраняет строку и FK restrict).
  4. Бизнес-правила и переходы.
    • soft-delete этапа (INV-STEP-3 уже проверен): DeletedAt = now, DeletedByUserId = ActorUserId, UpdatedAt = now, Version += 1; фиксируется доменный факт PipelineStepChanged;
    • перенос сделок (только при активных сделках): для активных сделок этапа StepId := TargetStepId, UpdatedAt := now, UpdatedByUserId := ActorUserId. StepGroup и Status сделок не меняются — приёмник той же системной группы (INV-STEP-4), поэтому денормализованная группа остаётся валидной.
  5. Что меняется. Этап помечается архивным (строка сохраняется, FK restrict удовлетворён); активные сделки массово переносятся в приёмник той же группы (set-based, без per-deal событий — осознанно, массовая административная реорганизация внутри группы). Перенос берёт строки под блокировкой (пропуская заблокированные), чтобы не дедлочить с параллельным drag-n-drop (MoveToStep). Конфликт версии строки при гонке архива/переименования → команда фейлится, клиент перечитывает доску и повторяет.
  6. События. Интеграционных Kafka-событий нет: этапы — внутренний справочник CRM; перенос происходит внутри одной группы (StepGroup/Status неизменны), а внешние потребители pin.crm.deal-stage-changed.v1 (deal-stage-changed.md) ориентируются на группу — значимой дельты для них нет, поэтому per-deal события не публикуются. Внутренний доменный факт PipelineStepChanged инвалидирует кэш доски/статистики групп.
  7. Согласованность. Массовый перенос активных сделок, soft-delete этапа и audit/Version фиксируются в одном согласованном изменении (атомарно).
  8. Результат. Тела ответа нет → HTTP 204 No Content.

Диаграмма

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

ТипДетали
Integration eventsnone — этапы внутренний справочник; перенос внутри группы не меняет StepGroup/Status сделок (см. шаг 6, deal-stage-changed.md)
External callsnone
Cache / projections / searchдоменное PipelineStepChanged → инвалидация кэша доски/группировок (внутренний подписчик)
Notificationsnone (административная реорганизация; участники сделок не уведомляются)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400StepId пуст, либо TargetStepId пуст / равен StepId«Проверьте корректность запроса»no retry
CRM_STEP_NOT_FOUND404этап не найден или уже архивирован (query-filter)«Этап не найден»no retry
CRM_STEP_FORBIDDEN403step.TenantId != TenantId актора (чужой застройщик)«Нет доступа к этапу»no retry
CRM_STEP_LAST_IN_GROUP409INV-STEP-3: последний живой этап группы (liveCount == 1)«Нельзя удалить единственный этап группы»no retry
CRM_STEP_HAS_DEALS_TARGET_REQUIRED409INV-STEP-4: на этапе есть активные сделки, TargetStepId не задан«Укажите этап-приёмник для активных сделок»no retry (повтор с TargetStepId)
CRM_STEP_TARGET_INVALID409приёмник не найден / архивирован / другой группы / равен исходному«Недопустимый этап-приёмник»no retry
CRM_STEP_CONCURRENCY_CONFLICT409конфликт RowVersion (параллельный архив/reorder этапа)«Этап изменён, обновите доску»retry (перечитать)

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

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

  • актор — менеджер застройщика (или Менеджер ПИН, platform-scope) с permission crm.pipeline.manage;
  • этап существует среди живых в TenantId = DeveloperCompanyId актора;
  • в его системной группе останется хотя бы один живой этап после архива (INV-STEP-3);
  • при наличии активных сделок на этапе задан валидный TargetStepId — живой этап той же группы (INV-STEP-4).

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

  • этап помечен архивным (DeletedAt, DeletedByUserId, Version +1); строка сохраняется (FK restrict), повторный архив даёт 404;
  • активные сделки (Status ∉ {Completed, Cancelled}) массово перенесены в TargetStepId; их StepGroup и Status не меняются (приёмник той же группы), поэтому денормализованная группа остаётся валидной;
  • терминальные сделки (Completed/Cancelled) остаются ссылаться на архивированный этап как история;
  • per-deal интеграционных событий pin.crm.deal-stage-changed.v1 не публикуется (перенос внутри группы не меняет StepGroup/Status, значимой дельты для внешних потребителей нет); внутренний факт PipelineStepChanged инвалидирует кэш доски/группировок; наружу окна eventual-consistency нет.

Acceptance Criteria

  • Happy path без сделок. Дано: живой этап без активных сделок, в группе есть другие живые этапы. Когда: архив без TargetStepId. Тогда: 204, этап архивен, переносить нечего.
  • С активными сделками и приёмником. Дано: на этапе есть активные сделки, задан валидный приёмник той же группы. Когда: архив. Тогда: 204, сделки перенесены в приёмник (movedDealsCount > 0), StepGroup/Status неизменны, per-deal событий нет.
  • С активными сделками без приёмника. Дано: активные сделки, TargetStepId == null. Когда: архив. Тогда: 409 CRM_STEP_HAS_DEALS_TARGET_REQUIRED (повтор с приёмником).
  • Последний этап группы. Дано: этап — единственный живой в группе. Когда: архив. Тогда: 409 CRM_STEP_LAST_IN_GROUP.
  • Недопустимый приёмник. Дано: TargetStepId другой группы / архивный / равен исходному. Когда: архив. Тогда: 409 CRM_STEP_TARGET_INVALID.
  • Терминальные сделки. Дано: на этапе есть завершённые/отменённые сделки. Когда: архив. Тогда: они не переносятся и остаются ссылаться на архивный этап (история сохранена).
  • Конкурентный reorder. Дано: этап параллельно переименовывают/переупорядочивают. Когда: гонка версии строки. Тогда: 409 CRM_STEP_CONCURRENCY_CONFLICT; клиент перечитывает доску и повторяет.

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

  • TargetStepId — optional additive-поле; переход на обязательность (при активных сделках) выражается доменным правилом INV-STEP-4, не контрактом. Удаление/переименование полей — breaking (migration + rollout).
  • Logs: pipeline-step archived (StepId, TenantId, Group, TargetStepId?, movedDealsCount); metrics: crm_pipeline_steps_archived_total, crm_pipeline_step_archive_moved_deals (histogram), latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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

  • Сущности: pipeline-step.md (агрегат PipelineStepEntity, INV-STEP-3/4, enum PipelineStepGroup, soft-delete, доменное PipelineStepChanged), deal.md (StepId/StepGroup, DealStatus).
  • События: deal-stage-changed.md (почему перенос внутри группы не публикуется наружу); карта области — README.
  • ADR: 0052 tenancy, 0056 simplicity-first.