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

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 areadeveloper
Feature grouppipeline
Статусdraft
Документdocs/06-services/crm/api/developer/create-step.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId)
Auth policy / Permissionscrm.pipeline.manage (Менеджер ПИН — platform-scope, тот же permission)
Scope (RBAC)организация (застройщик): менеджер настраивает этапы своей доски
Record-levelновый PipelineStepEntity.TenantId = actor.DeveloperCompanyId (этапы настраивает застройщик)
Tenant isolationTenantId = DeveloperCompanyId в каждом запросе (ADR-0052)
Auditcreate (CreatedByUserId = actor, инкремент Version)
Доступ

Permission crm.pipeline.manage действует в пределах своего TenantId: менеджер застройщика создаёт этапы только своей доски (TenantId = actor.DeveloperCompanyId); Менеджер ПИН — тот же permission, но platform-scope (сквозной доступ ко всем tenant).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/pipeline-steps
Success status201 (этап создан; тело — карточка этапа PipelineStepModel)
Idempotency headerне применяется — создание не идемпотентно (повтор даёт дубль по Name(ci)/Order → 409, INV-STEP-2)
Correlationсквозной trace через заголовок traceparent

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

Важно

Group — строковый enum, а не свободный код: набор системных групп фиксирован кодом/SEED, поэтому «существование группы» проверяется как принадлежность enum, без приёма произвольных строк.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyIdTenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера; record-level + CreatedByUserId
GroupbodyPipelineStepGroup (enum строкой)ДаEnum.IsDefined (значение enum = существующая SEED-группа, INV-STEP-1); иммутабельно после создания
NamebodystringДа1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых (INV-STEP-2)
ColorbodystringДаchar(7), regex ^#[0-9A-Fa-f]{6}$; default застройщика #9CA3AF, но в create задаётся явно
OrderbodyintДа>= 0; уникально по TenantId + Order среди живых (INV-STEP-2)

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

Проекция агрегата — дом полей pipeline-step.md; ниже только состав карточки этапа, возвращаемой create/list. TenantId наружу не отдаётся (внутренняя изоляция).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPipelineStepEntity.IdИдентификатор созданного этапа.
Groupstring (enum PipelineStepGroup)ДаPipelineStepEntity.GroupСистемная группа (иммутабельна).
NamestringДаPipelineStepEntity.NameНазвание этапа.
ColorstringДаPipelineStepEntity.ColorЦвет колонки/ярлыка #RRGGBB.
OrderintДаPipelineStepEntity.OrderПозиция на доске (сортировка: Group.Order, затем Order).
IsSeedDefaultboolДаPipelineStepEntity.IsSeedDefaultВсегда false для созданных этим методом.
CreatedAtDateTimeOffsetДаPipelineStepEntity.CreatedAtМомент создания.
VersionlongДаPipelineStepEntity.VersionОптимистическая версия (== 1 после создания).

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.pipeline.manage; менеджер ПИН — platform-scope); tenant — TenantId = DeveloperCompanyId. Создание намеренно не идемпотентно.
  2. Проверка входа и предусловий.
    • 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) в TenantIdCRM_STEP_NAME_DUPLICATE; живой этап с таким Order в TenantIdCRM_STEP_ORDER_DUPLICATE;
    • scope: только tenant isolation (TenantId = DeveloperCompanyId); родительских агрегатов нет.
  3. Что читаем. Родительских агрегатов нет (create). Читаем лишь SEED-группу для defence-in-depth (шаг 2) и наличие живого этапа с тем же именем/порядком. Иные этапы не нужны — новый этап зависит только от своей группы.
  4. Бизнес-правила и переходы. Создаётся этап: конструктор закрепляет INV-STEP-1 (Group обязателен и иммутабелен). Маппинг поле-в-поле: Id — новый идентификатор, TenantId = DeveloperCompanyId, Group, Name = trim(Name), Color, Order — из входа, IsSeedDefault = false, CreatedByUserId = ActorUserId, Version = 1. Фиксируется доменный факт PipelineStepChanged.
  5. Что меняется. Появляется новый этап доски. Гонку с параллельным create того же имени/порядка ловят partial-unique ux_pipeline_steps_tenant_name и ux_pipeline_steps_tenant_orderCRM_STEP_NAME_DUPLICATE / CRM_STEP_ORDER_DUPLICATE (pre-check шага 2 — для дружелюбного ответа на неконкурентном пути).
  6. События. Интеграционных Kafka-событий нет: этапы — внутренний справочник CRM, топика pin.crm.pipeline-step не существует; внешние контексты видят только StepGroup сделки в deal-stage-changed.md. Внутренний доменный факт PipelineStepChanged инвалидирует кэш доски.
  7. Согласованность. Вставка этапа фиксируется в одном согласованном изменении (атомарно). Нарушение unique-индекса откатывает изменение → duplicate-код (шаг 5).
  8. Результат. Карточка PipelineStepModel созданного этапа (Id, Group, Name, Color, Order, IsSeedDefault = false, Version = 1) → HTTP 201.

Диаграмма

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

ТипДетали
Integration eventsnone (этапы — внутренний справочник; kafka-топика нет)
External callsnone
Cache / projections / searchдоменное PipelineStepChanged → инвалидация проекции колонок доски застройщика (in-memory/kanban)
Notificationsnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Name пусто / Color не #RRGGBB / Order < 0 / Group не enum«Проверьте корректность полей этапа»no retry
CRM_STEP_GROUP_NOT_FOUND404Group отсутствует в SEED crm.pipeline_step_groups (рассинхрон enum/SEED)«Системная группа этапа не найдена»no retry
CRM_STEP_NAME_DUPLICATE409живой этап с таким Name (ci) уже есть в TenantId (pre-check либо ux_pipeline_steps_tenant_name)«Этап с таким названием уже существует»no retry
CRM_STEP_ORDER_DUPLICATE409живой этап с таким 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 не входит в enum PipelineStepGroup. Когда: создание. Тогда: 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). Enum PipelineStepGroup сериализуется строкой; на входе принимаются только определённые значения (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; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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

  • Сущность и инварианты: pipeline-step.md (INV-STEP-1 привязка к группе, INV-STEP-2 уникальность Name(ci)/Order, SEED PipelineStepGroup, индексы 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.