POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок
Назначение
Менеджер застройщика заводит именованную группу планировок («Студии», «Люксы с видом на море») в проекте
своей компании (SyncSource = Pin) — укрупнённый «тип объекта недвижимости», уровень иерархии между
проектом и планировкой. На группу затем настраиваются правила вознаграждений (rewards) и по ней
агрегируется статистика просмотров (dev-stats, ADR-0057). Дом полей — сущность
Группа планировок.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/create-layout-group.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.layouts.manage |
| Scope (RBAC) | организация / группа проектов / проект (projectId в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; LayoutGroup.TenantId наследуется от проекта (INV-LGR-1); чужой проект недостижим (404, не 403) |
| Audit | создание: CreatedByUserId, Version = 1 |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/projects/{projectId}/layout-groups |
| Success status | 201 (Location: /api/v1/developers/layout-groups/{id}) |
| Idempotency header | Idempotency-Key (опционально) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ProjectId | route | Guid | Да | существующий проект tenant-а, не Archived | 404 при чужом |
Name | body | string | Да | 1..128, не пустое после trim; уникально в проекте среди не удалённых (INV-LGR-2) | — |
Description | body | string? | Нет | 0..2000 (varchar(2000)) | — |
SortOrder | body | int | Нет (default 0) | 0..1000 | — |
Реплика-поля Talent (ExternalTalentLayoutGroupId, ReplicaVersion, LastSyncedAt) не принимаются — для
PIN-групп остаются null (layout-group.md, INV-LGR ряд Sync).
Модель ответа CreateDeveloperLayoutGroupResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutGroupEntity.Id | — | Id созданной группы. |
ProjectId | Guid | Да | ProjectId | — | Проект-владелец. |
Name | string | Да | Name | — | Нормализованное (trim) название. |
SortOrder | int | Да | SortOrder | — | Порядок отображения. |
Version | long | Да | Version | — | 1. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
Денормализации LayoutsCount = 0, Area = null наружу при создании не возвращаются — у новой группы
планировок ещё нет (пересчёт — background project-aggregates-recalc).
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manageв scope, покрывающемprojectId. При заголовкеIdempotency-Keyповтор возвращает сохранённыйIdбез повторной вставки и без повторного события; тот же ключ с иным телом →IDEMPOTENCY_KEY_MISMATCH(409). - Проверка входа и предусловий.
- длина
Name, длинаDescription, диапазонSortOrder— иначеValidationError; - проект-родитель существует в пределах tenant-а — иначе
CHESSBOARD_PROJECT_NOT_FOUND(404); - проект не архивирован (
Status != Archived, не удалён) — иначеCHESSBOARD_PROJECT_ARCHIVED(409, в архив группы не добавляются); - имя уникально в проекте среди не удалённых (INV-LGR-2) — иначе
CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN(409); гонка добивается unique indexux_layout_groups_project_name.
- длина
- Создание группы. Группа заводится с
SyncSource = Pin;TenantIdберётся из проекта (INV-LGR-1),ProjectId— от проекта. Поля берутся из формы (SortOrderпо умолчанию0); денормализацииLayoutsCount = 0,Area = null; проставляютсяCreatedByUserId,Version = 1. - Факты и события. Публикуется событие
pin.chessboard.layout-group-upserted.v1(снапшот группы). - Результат. Возвращаются
Id,ProjectId,Name,SortOrder,Version = 1,CreatedAt; HTTP201+Location.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutGroupUpsertedKafkaEvent → pin.chessboard.layout-group-upserted.v1 (key = Id); контракт — ../events/layout-group-upserted.md; потребители: dev-stats (измерение LayoutGroup, ADR-0057), rewards (условия правил по группе), catalog (группировка ассортимента) |
| Проекции / пересчёт | денормализации LayoutsCount/Area пересчитывает background project-aggregates-recalc при появлении планировок |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение Name/Description/SortOrder ограничений | Список ошибок полей | no retry |
CHESSBOARD_PROJECT_NOT_FOUND | 404 | Проект не найден / чужой tenant | «Проект не найден» | no retry |
CHESSBOARD_PROJECT_ARCHIVED | 409 | Проект в архиве (Status = Archived) | «Проект в архиве» | no retry |
CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN | 409 | Дубль имени в проекте (INV-LGR-2) | «Название группы занято» | no retry |
IDEMPOTENCY_KEY_MISMATCH | 409 | Тот же Idempotency-Key переиспользован с иным телом запроса | «Ключ идемпотентности переиспользован» | no retry |
FORBIDDEN | 403 | Нет chessboard.layouts.manage в scope | «Недостаточно прав» | no retry |
Сквозные UNAUTHORIZED/CHESSBOARD_CONCURRENCY_CONFLICT — в README.md.
Совместимость и наблюдаемость
- Новые optional поля тела — additive без breaking change; удаление/переименование/смена типа — breaking
(migration + rollout). Контракт события — additive-only в рамках
ContractVersion = 1(../domain/layout-group.md). - Logs:
TenantId,ProjectId,LayoutGroupId(PII нет); metrics:chessboard_developers_layout_group_create_total{result}, latency p95; traces — OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- Happy path. Дано: менеджер застройщика с
chessboard.layouts.manageв scope проекта; проект tenant-а существует и не архивирован. Когда:POSTс валиднымName. Тогда: создана группаSyncSource = Pin,Version = 1,LayoutsCount = 0,Area = null,TenantIdунаследован от проекта (INV-LGR-1);201+Location; опубликованоpin.chessboard.layout-group-upserted.v1(key =Id). - Дубль имени. Дано: в проекте уже есть не удалённая группа с тем же
Name. Когда: создание (в т.ч. гонка). Тогда:CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN(409, INV-LGR-2). - Проект чужой/архивный. Дано:
projectIdдругого tenant-а либо архивирован. Когда: создание. Тогда:CHESSBOARD_PROJECT_NOT_FOUND(404) /CHESSBOARD_PROJECT_ARCHIVED(409). - Границы полей. Дано:
Nameвне1..128,Description> 2000,SortOrderвне0..1000. Когда: создание. Тогда:ValidationError(400). - Идемпотентный повтор. Дано: первый вызов под
Idempotency-Key = K. Когда: повтор с тем жеKи телом → тот жеId; при ином теле →IDEMPOTENCY_KEY_MISMATCH(409). - Eventual consistency. После
201группа пуста; денормализацииLayoutsCount/Areaуточняются фоново (project-aggregates-recalc); измерение LayoutGroup вdev-statsи условия правилrewardsактивируются после потребления события (окно — SLO событий).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId}/layout-groups — Список групп планировок, POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки, PUT /api/v1/developers/layout-groups/{layoutGroupId} — Обновление группы планировок
- События (1): Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/layout-group.md (INV-LGR-1/2); родитель: ../domain/project.md.
- Контракт события: ../events/layout-group-upserted.md
(
LayoutGroupUpsertedKafkaEvent, snapshot, потребители dev-stats/rewards/catalog). - Соседние операции группы: list-layout-groups.md, update-layout-group.md, archive-layout-group.md; реестр — README.md.