POST /api/developer/crm/pipeline-steps — Создать этап
Менеджер застройщика добавляет настраиваемый этап kanban-воронки, обязательно привязанный к системной
группе PipelineStepGroup (инвариант INV-STEP-1). Доска у застройщика одна, уровень групп — read-only
SEED (меняется только миграциями), а этот метод оперирует нижним, настраиваемым уровнем: название, цвет
колонки, порядок. Дом полей, инвариантов и переходов — pipeline-step.md.
Назначение
Заводит новый этап воронки в TenantId = DeveloperCompanyId: выбирает системную группу (Group), задаёт
Name, Color (#RRGGBB) и позицию Order на доске. Вызывает менеджер застройщика; на выходе — карточка
созданного этапа.
INV-STEP-2: Name (без учёта регистра) и Order уникальны среди живых этапов TenantId — конфликт по любому
из полей завершается 409 CRM_STEP_NAME_DUPLICATE / CRM_STEP_ORDER_DUPLICATE, новый этап не создаётся.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | pipeline |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/create-step.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId) |
| Auth policy / Permissions | crm.pipeline.manage (Менеджер ПИН — platform-scope, тот же permission) |
| Scope (RBAC) | организация (застройщик): менеджер настраивает этапы своей доски |
| Record-level | новый PipelineStepEntity.TenantId = actor.DeveloperCompanyId (этапы настраивает застройщик) |
| Tenant isolation | TenantId = DeveloperCompanyId в каждом запросе (ADR-0052) |
| Audit | create (CreatedByUserId = actor, инкремент Version) |
Permission crm.pipeline.manage действует в пределах своего TenantId: менеджер застройщика создаёт этапы
только своей доски (TenantId = actor.DeveloperCompanyId); Менеджер ПИН — тот же permission, но
platform-scope (сквозной доступ ко всем tenant).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/pipeline-steps |
| Success status | 201 (этап создан; тело — карточка этапа PipelineStepModel) |
| Idempotency header | не применяется — создание не идемпотентно (повтор даёт дубль по Name(ci)/Order → 409, INV-STEP-2) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Group — строковый enum, а не свободный код: набор системных групп фиксирован кодом/SEED, поэтому
«существование группы» проверяется как принадлежность enum, без приёма произвольных строк.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера; record-level + CreatedByUserId | — |
Group | body | PipelineStepGroup (enum строкой) | Да | Enum.IsDefined (значение enum = существующая SEED-группа, INV-STEP-1); иммутабельно после создания | — |
Name | body | string | Да | 1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых (INV-STEP-2) | — |
Color | body | string | Да | char(7), regex ^#[0-9A-Fa-f]{6}$; default застройщика #9CA3AF, но в create задаётся явно | — |
Order | body | int | Да | >= 0; уникально по TenantId + Order среди живых (INV-STEP-2) | — |
Модель ответа PipelineStepModel
Проекция агрегата — дом полей pipeline-step.md; ниже только состав карточки
этапа, возвращаемой create/list. TenantId наружу не отдаётся (внутренняя изоляция).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PipelineStepEntity.Id | — | Идентификатор созданного этапа. |
Group | string (enum PipelineStepGroup) | Да | PipelineStepEntity.Group | — | Системная группа (иммутабельна). |
Name | string | Да | PipelineStepEntity.Name | — | Название этапа. |
Color | string | Да | PipelineStepEntity.Color | — | Цвет колонки/ярлыка #RRGGBB. |
Order | int | Да | PipelineStepEntity.Order | — | Позиция на доске (сортировка: Group.Order, затем Order). |
IsSeedDefault | bool | Да | PipelineStepEntity.IsSeedDefault | — | Всегда false для созданных этим методом. |
CreatedAt | DateTimeOffset | Да | PipelineStepEntity.CreatedAt | — | Момент создания. |
Version | long | Да | PipelineStepEntity.Version | — | Оптимистическая версия (== 1 после создания). |
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.pipeline.manage; менеджер ПИН — platform-scope); tenant —TenantId = DeveloperCompanyId. Создание намеренно не идемпотентно. - Проверка входа и предусловий.
Nameне пусто после trim (1..128);Colorматчит^#[0-9A-Fa-f]{6}$;Order >= 0;Group— определённое значениеPipelineStepGroup(INV-STEP-1) — иначеVALIDATION_FAILED;- существование группы: значение
Groupсоответствует SEED-справочникуcrm.pipeline_step_groups; при рассинхроне SEED/enum →CRM_STEP_GROUP_NOT_FOUND(нормально недостижимо при валидном enum, defence-in-depth); - уникальность (pre-check, INV-STEP-2): живой этап с таким
Name(ci) вTenantId→CRM_STEP_NAME_DUPLICATE; живой этап с такимOrderвTenantId→CRM_STEP_ORDER_DUPLICATE; - scope: только tenant isolation (
TenantId = DeveloperCompanyId); родительских агрегатов нет.
- Что читаем. Родительских агрегатов нет (create). Читаем лишь SEED-группу для defence-in-depth (шаг 2) и наличие живого этапа с тем же именем/порядком. Иные этапы не нужны — новый этап зависит только от своей группы.
- Бизнес-правила и переходы. Создаётся этап: конструктор закрепляет INV-STEP-1 (
Groupобязателен и иммутабелен). Маппинг поле-в-поле:Id— новый идентификатор,TenantId = DeveloperCompanyId,Group,Name = trim(Name),Color,Order— из входа,IsSeedDefault = false,CreatedByUserId = ActorUserId,Version = 1. Фиксируется доменный фактPipelineStepChanged. - Что меняется. Появляется новый этап доски. Гонку с параллельным create того же имени/порядка ловят
partial-unique
ux_pipeline_steps_tenant_nameиux_pipeline_steps_tenant_order→CRM_STEP_NAME_DUPLICATE/CRM_STEP_ORDER_DUPLICATE(pre-check шага 2 — для дружелюбного ответа на неконкурентном пути). - События. Интеграционных Kafka-событий нет: этапы — внутренний справочник CRM, топика
pin.crm.pipeline-stepне существует; внешние контексты видят толькоStepGroupсделки в deal-stage-changed.md. Внутренний доменный фактPipelineStepChangedинвалидирует кэш доски. - Согласованность. Вставка этапа фиксируется в одном согласованном изменении (атомарно). Нарушение unique-индекса откатывает изменение → duplicate-код (шаг 5).
- Результат. Карточка
PipelineStepModelсозданного этапа (Id,Group,Name,Color,Order,IsSeedDefault = false,Version = 1) → HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (этапы — внутренний справочник; kafka-топика нет) |
| External calls | none |
| Cache / projections / search | доменное PipelineStepChanged → инвалидация проекции колонок доски застройщика (in-memory/kanban) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Name пусто / Color не #RRGGBB / Order < 0 / Group не enum | «Проверьте корректность полей этапа» | no retry |
CRM_STEP_GROUP_NOT_FOUND | 404 | Group отсутствует в SEED crm.pipeline_step_groups (рассинхрон enum/SEED) | «Системная группа этапа не найдена» | no retry |
CRM_STEP_NAME_DUPLICATE | 409 | живой этап с таким Name (ci) уже есть в TenantId (pre-check либо ux_pipeline_steps_tenant_name) | «Этап с таким названием уже существует» | no retry |
CRM_STEP_ORDER_DUPLICATE | 409 | живой этап с таким Order уже есть в TenantId (pre-check либо ux_pipeline_steps_tenant_order) | «Этап с таким порядком уже существует» | no retry (изменить Order) |
Прочие инварианты этапа (CRM_STEP_GROUP_IMMUTABLE, CRM_STEP_LAST_IN_GROUP,
CRM_STEP_HAS_DEALS_TARGET_REQUIRED, INV-STEP-3..5) к create не относятся — см.
pipeline-step.md и команды update/reorder/archive.
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика (или Менеджер ПИН, platform-scope) с permission
crm.pipeline.manage; Group— определённое значениеPipelineStepGroup(существующая SEED-группа, INV-STEP-1);- нет живого этапа с тем же именем (ci) и нет живого этапа с тем же
OrderвTenantId(INV-STEP-2).
Постусловия (гарантированы после успеха):
- создан новый живой настраиваемый этап (
Version = 1,IsSeedDefault = false,CreatedByUserId) вTenantId; Groupзафиксирована и иммутабельна (INV-STEP-1); уникальность имени и порядка в tenant сохранена (INV-STEP-2);- интеграционных Kafka-событий нет; внутренний факт
PipelineStepChangedинвалидирует кэш доски — колонка появляется на канбане при следующем чтении (синхронный источник, окна eventual-consistency наружу нет); - существующие сделки и их
StepId/StepGroupне затронуты.
Acceptance Criteria
- Happy path. Дано: валидная SEED-группа, уникальные имя и
Order, корректныйColor. Когда: создание. Тогда:201, карточка сVersion = 1,IsSeedDefault = false. - Дубль имени. Дано: живой этап с таким именем (ci) уже есть. Когда: создание (в т.ч. гонка). Тогда:
409 CRM_STEP_NAME_DUPLICATE(INV-STEP-2). - Дубль порядка. Дано: живой этап с таким
Orderуже есть. Когда: создание. Тогда:409 CRM_STEP_ORDER_DUPLICATE(изменитьOrder). - Неизвестная группа. Дано:
Groupне входит в enumPipelineStepGroup. Когда: создание. Тогда:400 VALIDATION_FAILED; при рассинхроне enum/SEED —404 CRM_STEP_GROUP_NOT_FOUND(defence-in-depth). - Некорректный цвет. Дано:
Colorне соответствует#RRGGBB. Когда: создание. Тогда:400 VALIDATION_FAILED.
Совместимость и наблюдаемость
- Route/тело стабильны; добавление optional-полей в
PipelineStepModel— additive (без breaking); удаление/ переименование/смена типа — breaking (migration + rollout). EnumPipelineStepGroupсериализуется строкой; на входе принимаются только определённые значения (Enum.IsDefined) — unknown →VALIDATION_FAILED. - Logs:
pipeline-step create requested/created/duplicate(TenantId,Group,Order,StepId; PII нет); metrics:crm_pipeline_step_create_total{result}; 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 привязка к группе,
INV-STEP-2 уникальность
Name(ci)/Order, SEEDPipelineStepGroup, индексыux_pipeline_steps_tenant_name/ux_pipeline_steps_tenant_order); использование этапа сделками: deal.md (StepId/StepGroup); карта событий и область developer — ../../README.md. - Событие с
StepGroupнаружу: deal-stage-changed.md (единственный канал, где внешние контексты видят уровень групп; сами этапы не публикуются). - ADR: 0052 tenancy, 0056 simplicity-first.