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 area | developer (JWT менеджера застройщика) |
| Feature group | pipeline |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/archive-step.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope) |
| Auth policy / Permissions | crm.pipeline.manage (менеджеры застройщика правят состав/порядок этапов) |
| Scope (RBAC) | организация (застройщик): record-level по PipelineStepEntity.TenantId |
| Record-level | step.TenantId = actor.TenantId (свой застройщик) либо Менеджер ПИН (platform-scope) |
| Tenant isolation | TenantId в запросе = step.TenantId = DeveloperCompanyId; кросс-tenant запрещён (кроме platform-scope) |
| Audit | delete (DeletedByUserId/UpdatedByUserId = actor.UserId); переносимые сделки — UpdatedByUserId. PII отсутствует |
Архивировать этап может только менеджер застройщика — владелец tenant этапа (step.TenantId = actor.TenantId)
— либо Менеджер ПИН (Operator, platform-scope, сквозной доступ). Кросс-tenant доступ запрещён
(CRM_STEP_FORBIDDEN).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/pipeline-steps/{stepId}/archive |
| Success status | 204 (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.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
StepId | route | Guid | Да | != Guid.Empty; существующий живой этап TenantId = актор (шаг 2) | — |
TargetStepId | body | Guid? | Нет (обязателен при активных сделках, INV-STEP-4) | != Guid.Empty; != StepId; живой этап той же Group, TenantId = актор (шаг 2–3) | — |
TenantId | identity claims | Guid | Да | Required; = step.TenantId (DeveloperCompanyId) | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика; permission crm.pipeline.manage | — |
Модель ответа
Отсутствует: по успеху HTTP 204 No Content. Обновлённая доска запрашивается отдельным list-методом области
developer. Тело/PII не возвращаются.
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.pipeline.manage) либо менеджер ПИН (platform-scope); tenant —TenantId = DeveloperCompanyId. Идемпотентность не требуется — повтор на архивированном этапе отсекается soft-delete-фильтром (шаг 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 == null→CRM_STEP_HAS_DEALS_TARGET_REQUIRED; - валидность приёмника (если задан): он живой, того же tenant и той же
Group, что архивируемый этап — иначеCRM_STEP_TARGET_INVALID.
- Что читаем. Сам этап (для мутации) и счётчики: число живых этапов группы (INV-STEP-3) и число активных
сделок этапа (INV-STEP-4) — без материализации строк. При активных сделках приёмник уже провалидирован.
Терминальные сделки (
Completed/Cancelled) на этапе не переносятся — они остаются ссылаться на архивированный этап как история (soft-delete сохраняет строку и FK restrict). - Бизнес-правила и переходы.
- 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), поэтому денормализованная группа остаётся валидной.
- soft-delete этапа (INV-STEP-3 уже проверен):
- Что меняется. Этап помечается архивным (строка сохраняется, FK restrict удовлетворён); активные сделки
массово переносятся в приёмник той же группы (set-based, без per-deal событий — осознанно, массовая
административная реорганизация внутри группы). Перенос берёт строки под блокировкой (пропуская
заблокированные), чтобы не дедлочить с параллельным drag-n-drop (
MoveToStep). Конфликт версии строки при гонке архива/переименования → команда фейлится, клиент перечитывает доску и повторяет. - События. Интеграционных Kafka-событий нет: этапы — внутренний справочник CRM; перенос происходит
внутри одной группы (
StepGroup/Statusнеизменны), а внешние потребителиpin.crm.deal-stage-changed.v1(deal-stage-changed.md) ориентируются на группу — значимой дельты для них нет, поэтому per-deal события не публикуются. Внутренний доменный фактPipelineStepChangedинвалидирует кэш доски/статистики групп. - Согласованность. Массовый перенос активных сделок, soft-delete этапа и audit/
Versionфиксируются в одном согласованном изменении (атомарно). - Результат. Тела ответа нет → HTTP
204 No Content.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none — этапы внутренний справочник; перенос внутри группы не меняет StepGroup/Status сделок (см. шаг 6, deal-stage-changed.md) |
| External calls | none |
| Cache / projections / search | доменное PipelineStepChanged → инвалидация кэша доски/группировок (внутренний подписчик) |
| Notifications | none (административная реорганизация; участники сделок не уведомляются) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | StepId пуст, либо TargetStepId пуст / равен StepId | «Проверьте корректность запроса» | no retry |
CRM_STEP_NOT_FOUND | 404 | этап не найден или уже архивирован (query-filter) | «Этап не найден» | no retry |
CRM_STEP_FORBIDDEN | 403 | step.TenantId != TenantId актора (чужой застройщик) | «Нет доступа к этапу» | no retry |
CRM_STEP_LAST_IN_GROUP | 409 | INV-STEP-3: последний живой этап группы (liveCount == 1) | «Нельзя удалить единственный этап группы» | no retry |
CRM_STEP_HAS_DEALS_TARGET_REQUIRED | 409 | INV-STEP-4: на этапе есть активные сделки, TargetStepId не задан | «Укажите этап-приёмник для активных сделок» | no retry (повтор с TargetStepId) |
CRM_STEP_TARGET_INVALID | 409 | приёмник не найден / архивирован / другой группы / равен исходному | «Недопустимый этап-приёмник» | no retry |
CRM_STEP_CONCURRENCY_CONFLICT | 409 | конфликт RowVersion (параллельный архив/reorder этапа) | «Этап изменён, обновите доску» | retry (перечитать) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика (или Менеджер ПИН, platform-scope) с permission
crm.pipeline.manage; - этап существует среди живых в
TenantId = DeveloperCompanyIdактора; - в его системной группе останется хотя бы один живой этап после архива (INV-STEP-3);
- при наличии активных сделок на этапе задан валидный
TargetStepId— живой этап той же группы (INV-STEP-4).
Постусловия (гарантированы после успеха):
- этап помечен архивным (
DeletedAt,DeletedByUserId,Version+1); строка сохраняется (FKrestrict), повторный архив даёт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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-003. Движение сделки по канбан-доске
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: pipeline-step.md (агрегат
PipelineStepEntity, INV-STEP-3/4, enumPipelineStepGroup, soft-delete, доменноеPipelineStepChanged), deal.md (StepId/StepGroup,DealStatus). - События: deal-stage-changed.md (почему перенос внутри группы не публикуется наружу); карта области — README.
- ADR: 0052 tenancy, 0056 simplicity-first.