PUT /api/developer/crm/pipeline-steps/{stepId} — Переименовать/перекрасить этап
Менеджер застройщика правит название и цвет настраиваемого этапа своей kanban-доски (застройщик
«создаёт, переименовывает, меняет порядок и цвет» этапы). Операция трогает только Name/Color: привязка к
системной группе Group иммутабельна (INV-STEP-1, история сделок и статистика считаются по группе), а
порядок Order меняет отдельная команда reorder. Дом полей, инвариантов и индексов —
pipeline-step.md.
Назначение
Обновить отображаемые атрибуты одного этапа воронки застройщика (Name, Color) с сохранением уникальности
имени в рамках tenant (INV-STEP-2) и оптимистической блокировкой по RowVersion (параллельная правка
имени/цвета/порядка тем же или другим менеджером).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | pipeline |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/update-step.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId; Менеджер ПИН — platform-scope) |
| Auth policy / Permissions | crm.pipeline.manage (настройка доски застройщика) |
| Scope (RBAC) | организация (застройщик): менеджер оперирует этапами своего TenantId |
| Record-level | PipelineStepEntity.TenantId == actor.DeveloperCompanyId (этап принадлежит доске застройщика) |
| Tenant isolation | TenantId = DeveloperCompanyId в каждом запросе (ADR-0052) |
| Audit | update (UpdatedByUserId = actor, UpdatedAt, инкремент Version) |
Правит этап менеджер застройщика — владелец доски по tenant (TenantId = DeveloperCompanyId), либо
Менеджер ПИН (platform-scope). Этап чужого tenant не раскрывается: обращение к нему даёт 403 CRM_STEP_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/developer/crm/pipeline-steps/{stepId} |
| Success status | 200 (этап обновлён; тело — обновлённая проекция этапа) |
| Idempotency header | не применяется — идемпотентность естественная (повтор с тем же Name/Color/RowVersion даёт тот же результат; устаревший RowVersion → 409) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
StepId | route | Guid | Да | != Guid.Empty; существующий живой этап в TenantId (шаг 2) | — |
Name | body | string | Да | trim, 1..128, не пустое; уникально по lower(Name) среди живых этапов tenant кроме себя (INV-STEP-2) | — |
Color | body | string | Да | формат #RRGGBB, regex ^#[0-9A-Fa-f]{6}$ (char(7)) | — |
Group | body | string? (enum PipelineStepGroup) | Нет | если задан — обязан совпасть с текущим step.Group; смена запрещена (INV-STEP-1) | — |
RowVersion | body | uint | Да | оптимистический токен, полученный из list/get; несовпадение → 409 | — |
ActorUserId | identity claims | Guid | Да | JWT менеджера; пишется в UpdatedByUserId | — |
Order в теле не принимается — позицию на доске меняет отдельная команда reorder (пакетный сдвиг
Order под своим unique-индексом). Group вынесен в тело только ради явного отказа при попытке смены
(иначе поле можно опустить).
Модель ответа PipelineStepModel
Проекция агрегата — дом полей pipeline-step.md; ниже перечислена только дельта, значимая для правки. Та же модель отдаётся list/get этапов застройщика (полный состав — в доме).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PipelineStepEntity.Id | — | Идентификатор этапа. |
Group | string (enum PipelineStepGroup) | Да | PipelineStepEntity.Group | — | Системная группа (не изменилась операцией). |
Name | string | Да | PipelineStepEntity.Name | — | Новое название этапа. |
Color | string | Да | PipelineStepEntity.Color | — | Новый цвет #RRGGBB. |
Order | int | Да | PipelineStepEntity.Order | — | Позиция на доске (не изменилась операцией). |
IsSeedDefault | bool | Да | PipelineStepEntity.IsSeedDefault | — | Дефолтный SEED-этап (для UI-подсказок). |
Version | long | Да | PipelineStepEntity.Version | — | Инкрементирована на 1. |
RowVersion | uint | Да | PipelineStepEntity.RowVersion | — | Новый токен оптимистической блокировки для следующей правки клиента. |
UpdatedAt | DateTimeOffset | Да | PipelineStepEntity.UpdatedAt | — | Момент правки. |
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.pipeline.manage; менеджер ПИН — platform-scope); tenant —TenantId = DeveloperCompanyId. Идемпотентность естественная — защита от параллельной правки строится наRowVersion(шаги 2, 5). - Проверка входа и предусловий.
StepId != Guid.Empty;Nameпосле trim1..128и не пустое;Colorсоответствует^#[0-9A-Fa-f]{6}$;RowVersionзадан — иначеVALIDATION_FAILED;- этап есть среди живых в
TenantId(архивный невидим) — иначеCRM_STEP_NOT_FOUND(правка архива запрещена); - scope:
step.TenantId == TenantId— иначеCRM_STEP_FORBIDDEN(чужая доска); - иммутабельность группы (INV-STEP-1): если
Groupзадан иGroup != step.Group→CRM_STEP_GROUP_IMMUTABLE(сменить группу этапа нельзя — история/статистика по группе); - уникальность имени (INV-STEP-2): живой этап того же tenant с таким
Name(ci), кроме себя — если есть →CRM_STEP_NAME_DUPLICATE.
- Что читаем. Только сам этап (single-aggregate update). Связанные сделки не читаются
(переименование/перекрас не трогают привязку сделок к этапу). Строчная блокировка не берётся —
конкурентность точечной правки покрывает оптимистический
RowVersion(в отличие от reorder/archive, которым нужна пессимистичная блокировка порядка). - Бизнес-правила и переходы. Правятся только
Name/Color;Groupне редактируется (INV-STEP-1 соблюдается конструкцией). Маппинг:Name = trim(Name),Color,UpdatedByUserId = ActorUserId,UpdatedAt = now,Version += 1;Order/IsSeedDefault/CreatedBy*не меняются. Статусной машины у этапа нет (живой ↔ архив — отдельной командой). Фиксируется доменный фактPipelineStepChanged. - Что меняется. Обновляются
Name/Colorэтапа. Оптимистическая блокировка поRowVersion: если между чтением клиента и записью этап был изменён (правка/reorder) — конфликтCRM_STEP_CONCURRENCY_CONFLICT; гонку вставки одинакового имени ловит unique-индексux_pipeline_steps_tenant_name→CRM_STEP_NAME_DUPLICATE. Сделки на этапе не перечитываются (FKdeals.step_idбез изменений). - События. Интеграционных Kafka-событий нет: этап — внутренний справочник CRM, внешние контексты видят
только
StepGroup, а он не изменился (см. deal-stage-changed.md). Внутренний доменный фактPipelineStepChangedинвалидирует кэш доски. - Согласованность. Обновление этапа и audit/
Versionфиксируются в одном согласованном изменении (атомарно). - Результат. Карточка
PipelineStepModelобновлённого этапа (новыеName/Color, инкрементированныйVersion, свежийRowVersion) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (правка имени/цвета kafka-событий не публикует; StepGroup не изменился) |
| External calls | none |
| Cache / projections / search | доменное PipelineStepChanged инвалидирует кэш kanban-доски застройщика; сделки на этапе не перечитываются (FK deals.step_id без изменений) |
| Notifications | none (настроечная операция; уведомления участникам не шлются) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | StepId = Guid.Empty, пустое/длинное Name, Color не #RRGGBB, нет RowVersion | «Проверьте корректность полей этапа» | no retry |
CRM_STEP_NOT_FOUND | 404 | этап не найден в TenantId либо архивирован | «Этап не найден» | no retry |
CRM_STEP_FORBIDDEN | 403 | чужой tenant / нет crm.pipeline.manage | «Нет доступа к этапу» | no retry |
CRM_STEP_GROUP_IMMUTABLE | 409 | тело содержит Group != step.Group (INV-STEP-1) | «Группу этапа изменить нельзя» | no retry |
CRM_STEP_NAME_DUPLICATE | 409 | имя занято живым этапом tenant (INV-STEP-2 / unique-индекс) | «Этап с таким названием уже есть» | no retry (сменить имя) |
CRM_STEP_CONCURRENCY_CONFLICT | 409 | RowVersion устарел (параллельная правка/reorder) | «Этап изменён параллельно, обновите данные» | retry (перечитать RowVersion) |
Acceptance Criteria
- Given живой этап в
TenantIdс актуальнымRowVersion, When валидныеName/Color, ThenName/Colorобновлены,Group/Order/IsSeedDefault/CreatedBy*неизменны,Version += 1, свежийRowVersion, ответ200. - Given тело содержит
Group != step.Group, When update, ThenCRM_STEP_GROUP_IMMUTABLE(409) по INV-STEP-1 — группу этапа сменить нельзя (история/статистика считаются по группе). - Given имя занято другим живым этапом того же tenant (ci), When update, Then
CRM_STEP_NAME_DUPLICATE(409) по INV-STEP-2 (pre-check илиux_pipeline_steps_tenant_name). - Given
RowVersionустарел (параллельная правка или reorder), When update, ThenCRM_STEP_CONCURRENCY_CONFLICT(409); клиент перечитывает и повторяет. - Given
Colorне соответствует^#[0-9A-Fa-f]{6}$, пустое после trimNameилиStepId = Guid.Empty, When update, ThenVALIDATION_FAILED(400). - Given этап архивирован либо не существует в
TenantId, When update, ThenCRM_STEP_NOT_FOUND(404); чужая доска →CRM_STEP_FORBIDDEN(403). - Given валидная правка
Name/Color, When изменения сохранены, Then привязки сделок (FKdeals.step_id) не меняются, внешняяStepGroupне меняется, Kafka-события не публикуются, доменныйPipelineStepChangedлишь инвалидирует кэш доски.
Совместимость и наблюдаемость
- Route/тело стабильны; добавление optional-полей в
PipelineStepModel— additive (без breaking); удаление/переименование/смена типа — breaking (migration + rollout). EnumPipelineStepGroup— строкой; неизвестное значение вGroupна входе →VALIDATION_FAILED(строгая типизация десериализатора). - Logs:
pipeline-step-update requested/succeeded/conflict(StepId,TenantId,Group; без PII); metrics:crm_pipeline_step_update_total{result}, 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 (INV-STEP-1 иммутабельность
Group, INV-STEP-2 уникальность имени,RowVersion/soft-delete, доменноеPipelineStepChanged); использование этапа сделками — deal.md. - События: карта — ../../README.md; внешняя видимость группы — deal-stage-changed.md (интеграционных событий этап не публикует).
- ADR: 0052 tenancy, 0056 simplicity-first.