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

POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок

Назначение

Менеджер застройщика заводит именованную группу планировок («Студии», «Люксы с видом на море») в проекте своей компании (SyncSource = Pin) — укрупнённый «тип объекта недвижимости», уровень иерархии между проектом и планировкой. На группу затем настраиваются правила вознаграждений (rewards) и по ней агрегируется статистика просмотров (dev-stats, ADR-0057). Дом полей — сущность Группа планировок.

Metadata

ПолеЗначение
Сервис/контекстchessboard
API areadevelopers
Feature grouplayouts
Статусdraft
Документdocs/06-services/chessboard/api/developers/create-layout-group.md

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

ПроверкаЗначение
Actor typeUser (менеджер застройщика)
Auth policy / Permissionschessboard.layouts.manage
Scope (RBAC)организация / группа проектов / проект (projectId в scope)
Tenant isolationTenantId = CompanyId из JWT; LayoutGroup.TenantId наследуется от проекта (INV-LGR-1); чужой проект недостижим (404, не 403)
Auditсоздание: CreatedByUserId, Version = 1

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/projects/{projectId}/layout-groups
Success status201 (Location: /api/v1/developers/layout-groups/{id})
Idempotency headerIdempotency-Key (опционально)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ProjectIdrouteGuidДасуществующий проект tenant-а, не Archived404 при чужом
NamebodystringДа1..128, не пустое после trim; уникально в проекте среди не удалённых (INV-LGR-2)
Descriptionbodystring?Нет0..2000 (varchar(2000))
SortOrderbodyintНет (default 0)0..1000
Реплика-поля Talent

Реплика-поля Talent (ExternalTalentLayoutGroupId, ReplicaVersion, LastSyncedAt) не принимаются — для PIN-групп остаются null (layout-group.md, INV-LGR ряд Sync).

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаLayoutGroupEntity.IdId созданной группы.
ProjectIdGuidДаProjectIdПроект-владелец.
NamestringДаNameНормализованное (trim) название.
SortOrderintДаSortOrderПорядок отображения.
VersionlongДаVersion1.
CreatedAtDateTimeOffsetДаCreatedAtМомент создания.
Денормализации не возвращаются

Денормализации LayoutsCount = 0, Area = null наружу при создании не возвращаются — у новой группы планировок ещё нет (пересчёт — background project-aggregates-recalc).

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.layouts.manage в scope, покрывающем projectId. При заголовке Idempotency-Key повтор возвращает сохранённый Id без повторной вставки и без повторного события; тот же ключ с иным телом → IDEMPOTENCY_KEY_MISMATCH (409).
  2. Проверка входа и предусловий.
    • длина 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 index ux_layout_groups_project_name.
  3. Создание группы. Группа заводится с SyncSource = Pin; TenantId берётся из проекта (INV-LGR-1), ProjectId — от проекта. Поля берутся из формы (SortOrder по умолчанию 0); денормализации LayoutsCount = 0, Area = null; проставляются CreatedByUserId, Version = 1.
  4. Факты и события. Публикуется событие pin.chessboard.layout-group-upserted.v1 (снапшот группы).
  5. Результат. Возвращаются Id, ProjectId, Name, SortOrder, Version = 1, CreatedAt; HTTP 201 + Location.

Диаграмма

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

ТипДетали
Интеграционные событияLayoutGroupUpsertedKafkaEventpin.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
ValidationError400Нарушение Name/Description/SortOrder ограниченийСписок ошибок полейno retry
CHESSBOARD_PROJECT_NOT_FOUND404Проект не найден / чужой tenant«Проект не найден»no retry
CHESSBOARD_PROJECT_ARCHIVED409Проект в архиве (Status = Archived)«Проект в архиве»no retry
CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN409Дубль имени в проекте (INV-LGR-2)«Название группы занято»no retry
IDEMPOTENCY_KEY_MISMATCH409Тот же Idempotency-Key переиспользован с иным телом запроса«Ключ идемпотентности переиспользован»no retry
FORBIDDEN403Нет 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); dashboard chessboard-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 событий).

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

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

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