Entity / Aggregate: PipelineStepEntity + SEED enum PipelineStepGroup
Дом полей многоуровневой системы этапов воронки контекста crm. Модель двухуровневая: системные группы
этапов (SEED + enum, меняются только через код/миграции) и настраиваемые этапы застройщика
(цвет/порядок/название). Досок/воронок несколько не бывает — доска одна.
Назначение
PipelineStepGroup— системная группа этапов kanban-доски: enum в коде + SEED-справочник. Никто, кроме владельцев платформы, не может менять группы (только код и миграции).PipelineStepEntity— настраиваемый этап воронки застройщика: менеджеры застройщика создают, удаляют (архивируют), переименовывают, меняют порядок и цвет; каждый этап обязательно привязан к системной группе. Сделки (deal.md) лежат внутри этапов (Deal.StepId).
SEED enum PipelineStepGroup (системные группы)
Хранится в коде (enum, как text) + read-only SEED-таблица crm.pipeline_step_groups для FK-целостности и
display-метаданных. Изменение набора — только миграцией.
| Значение | Order | Display (SEED) | Семантика (маппинг на DealStatus) |
|---|---|---|---|
Application | 10 | Заявка | Сделка создана; работа до брони (DealStatus.Application). |
Booking | 20 | Бронирование | Резерв/бронь объекта, документы и оплата брони (DealStatus.Booking). |
Contract | 30 | Договор | Сбор документов ДДУ/ДКП, подготовка/подписание договора (DealStatus.Sale). |
Payment | 40 | Оплата | Оплата по договору, эскроу (DealStatus.Sale). |
Registration | 50 | Регистрация | Регистрация в Росреестре (DealStatus.Sale). |
Completed | 60 | Завершено | Сделка завершена (DealStatus.Completed). Терминальная группа. |
Cancelled | 70 | Отказ | Сделка прекращена (DealStatus.Cancelled). Терминальная группа. |
Способ оплаты влияет на этапы: для PaymentMethod = Mortgage внутри групп Contract/Payment
застройщик обычно добавляет этапы «Одобрение ипотеки», «Кредитный договор»; для Installment — этапы
графика платежей. Набор групп при этом не меняется — гибкость даёт настраиваемый уровень.
SEED-таблица crm.pipeline_step_groups (read-only справочник)
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Code | PipelineStepGroup | Да | PK, enum как text, varchar(32) | SEED-миграция | Код группы (enum). |
Name | string | Да | varchar(128) | SEED | Отображаемое имя (русское). |
Order | int | Да | unique ux_step_groups_order | SEED | Порядок колонок групп на доске. |
IsTerminal | bool | Да | default false | SEED | Completed/Cancelled = true (guard переходов). |
Классификация (PipelineStepEntity)
| Поле | Значение |
|---|---|
| Контекст | crm |
| Kind | AggregateRoot (простой настраиваемый справочник — CRUD без бизнес-логики) |
| Source of truth | crm |
| Таблица | crm.pipeline_steps |
| Tenant isolation | TenantId = DeveloperCompanyId (этапы настраивает застройщик) |
Поля (PipelineStepEntity)
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор шага. |
TenantId | Guid | Да | index ix_pipeline_steps_tenant_id; часть unique-индексов | identity (actor застройщика) / SEED | Застройщик-владелец этапа. |
Group | PipelineStepGroup | Да | FK fk_pipeline_steps_group → crm.pipeline_step_groups(code), enum как text, varchar(32), index | app | Обязательная привязка к системной группе. Иммутабельно после создания (история сделок и статистика считаются по группе). |
Name | string | Да | varchar(128), не пустое; unique ux_pipeline_steps_tenant_name on (TenantId, lower(Name)) where deleted_at is null | app | Название этапа. |
Color | string | Да | char(7), формат #RRGGBB (regex ^#[0-9A-Fa-f]{6}$), default #9CA3AF | app | Цвет колонки/ярлыка. |
Order | int | Да | >= 0; unique ux_pipeline_steps_tenant_order on (TenantId,Order) where deleted_at is null, deferrable (пакетный reorder) | app / SEED | Позиция на доске. Глобальная сортировка доски: Group.Order, затем Order. |
IsSeedDefault | bool | Да | default false | SEED | Этап создан дефолтным SEED-набором застройщика (по одному этапу на группу при онбординге компании); удалять можно, флаг — для UI-подсказок. |
CreatedAt | DateTimeOffset | Да | — | app | Создание. |
CreatedByUserId | Guid? | Нет | — | app | Менеджер застройщика (null — SEED). |
UpdatedAt | DateTimeOffset | Да | — | app | Дата последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Архив этапа. |
DeletedByUserId | Guid? | Нет | — | app | Кто заархивировал. |
Value objects
Value objects не используются (простой справочник).
Модели JSONB (versioned)
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
pipeline_step_groups (SEED) | reference | Group | in-memory enum + справочник | restrict |
DealEntity | reference (children) | deals.step_id | выборка по (TenantId,StepId) — колонки канбана | restrict (архив вместо удаления при наличии сделок) |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-STEP-1 | Каждый этап привязан к существующей системной группе; Group иммутабелен | при создании (поле не редактируется) | CRM_STEP_GROUP_IMMUTABLE |
| INV-STEP-2 | В рамках TenantId живые этапы уникальны по имени (case-insensitive) и по Order | при создании/переименовании/reorder (unique-индексы) | CRM_STEP_NAME_DUPLICATE / CRM_STEP_ORDER_DUPLICATE |
| INV-STEP-3 | На каждую группу у застройщика — минимум один живой этап (иначе сделкам некуда попадать при авто-переходах) | при архивировании этапа | CRM_STEP_LAST_IN_GROUP |
| INV-STEP-4 | Архивирование этапа с активными сделками требует указания этапа-приёмника той же группы; сделки переносятся в одном согласованном изменении | при архивировании этапа с указанием этапа-приёмника | CRM_STEP_HAS_DEALS_TARGET_REQUIRED |
| INV-STEP-5 | Системные группы не редактируются через API (только SEED-миграции) | нет операций над группами | CRM_STEP_GROUP_READONLY |
Диаграмма состояний неприменима — у этапа нет статусной машины (живой ↔ архив через soft-delete); переходы сделок по этапам — deal.md.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
PipelineStepChanged | доменное | create/update/reorder/archive | Id, TenantId, Group, Order |
Этапы — внутренний справочник CRM; внешние контексты видят только StepGroup в
pin.crm.deal-stage-changed.v1 (deal.md).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_pipeline_steps_tenant_id; (tenant_id,group); ux_pipeline_steps_tenant_name unique partial; ux_pipeline_steps_tenant_order unique partial deferrable |
| Партиционирование / retention | none (десятки строк на застройщика) |
| Concurrency | оптимистическая блокировка — reorder-конфликты |
| Permissions / PII | crm.pipeline.view (все участники CRM своего застройщика/сделок), crm.pipeline.manage (менеджеры застройщика; Менеджер ПИН — platform-scope). PII отсутствует. |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete |
Целостность, конкуренция и восстановление
- Иммутабельная группа.
Groupэтапа не редактируется после создания (INV-STEP-1) →CRM_STEP_GROUP_IMMUTABLE; история сделок и статистика считаются по группе, поэтому переклейка группы исказила бы отчётность. - Уникальность имени и порядка. Живые этапы застройщика уникальны по имени (case-insensitive) и по
Order(INV-STEP-2) →CRM_STEP_NAME_DUPLICATE/CRM_STEP_ORDER_DUPLICATE. Пакетный reorder использует deferrable-уникальностьOrder, поэтому промежуточные состояния внутри одного согласованного изменения допустимы; конкурентный reorder разрешается оптимистической блокировкой (проигравший перечитывает доску). - Минимум один этап на группу. Архивировать последний живой этап группы нельзя (INV-STEP-3) →
CRM_STEP_LAST_IN_GROUP; иначе авто-переходам сделок некуда попадать. - Архив с активными сделками. Архивирование этапа со сделками требует указания этапа-приёмника той же
группы; сделки переносятся в одном согласованном изменении (INV-STEP-4) → иначе
CRM_STEP_HAS_DEALS_TARGET_REQUIRED.StepGroupсделок при переносе не меняется (та же группа). - Системные группы read-only. Набор
PipelineStepGroupменяется только SEED-миграциями (INV-STEP-5); API-операций над группами нет →CRM_STEP_GROUP_READONLY. - Отдача наружу. Этапы — внутренний справочник; внешние контексты видят только
StepGroupвpin.crm.deal-stage-changed.v1(deal.md), поэтому переименование/перекраска этапа безопасны для интеграций.
Acceptance-критерии
- Happy (создание этапа). Дано: у застройщика есть группа
Booking. Когда менеджер создаёт этап «Ожидает оплаты» в этой группе. Тогда этап привязан к группе, уникален по имени/порядку. - Дубль имени/порядка. Дано: живой этап с именем/порядком X. Когда создаётся второй с тем же. Тогда
отказ
CRM_STEP_NAME_DUPLICATE/CRM_STEP_ORDER_DUPLICATE. - Смена группы. Дано: существующий этап. Когда пытаются сменить
Group. Тогда отказCRM_STEP_GROUP_IMMUTABLE. - Архив последнего в группе. Дано: в группе один живой этап. Когда его архивируют. Тогда отказ
CRM_STEP_LAST_IN_GROUP. - Архив с сделками без приёмника. Дано: на этапе есть сделки. Когда его архивируют без этапа-приёмника.
Тогда отказ
CRM_STEP_HAS_DEALS_TARGET_REQUIRED; при указании приёмника той же группы сделки переносятся.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: DealEntity
- API (18): GET /api/agent/crm/deals — Сделки агента (список/поиск), GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/pipeline-steps — Этапы воронки застройщика, GET /api/developer/crm/statistics — Статистика ЛК застройщика, GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор), GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы, GET /api/pin-manager/crm/deals/{dealId} — Карточка любой сделки (Менеджер ПИН), POST /api/agent/crm/deals — Создать сделку из лида по объекту, POST /api/developer/crm/deals/{dealId}/contracts-prepared — Отметить «Договоры подготовлены», POST /api/developer/crm/deals/{dealId}/rosreestr — Подтвердить регистрацию Росреестра, POST /api/developer/crm/pipeline-steps — Создать этап (+6)
- Use Cases (3): UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-003. Движение сделки по канбан-доске, UC-CRM-006. Вмешательство Менеджера ПИН
- События (2): Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- deal.md —
StepId/StepGroup, правилаMoveToStep. - README контекста — SEED-набор этапов застройщика.