POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки
Назначение
Менеджер застройщика заводит типовую планировку проекта своей компании (SyncSource = Pin): комнатность,
площади, схемы (2D/3D-план, положение на этаже, визуализации) и опциональную привязку к группе планировок.
На планировку далее ссылаются объекты недвижимости (PropertyObject.LayoutId), правила вознаграждений
(rewards) и эконом-модель по типам объектов (invest-analytics); dev-stats использует её как уровень
фильтрации статистики (ADR-0057). Дом полей — сущность Планировка.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/create-layout.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.layouts.manage |
| Scope (RBAC) | организация / группа проектов / проект (projectId в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; Layout.TenantId наследуется от проекта (INV-LAY-1); чужой проект недостижим (404, не 403) |
| Audit | создание: CreatedByUserId, Version = 1 |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/projects/{projectId}/layouts |
| Success status | 201 (Location: /api/v1/developers/layouts/{id}) |
| Idempotency header | Idempotency-Key (опционально; повтор с тем же ключом возвращает первый результат) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Ограничения полей — из дома ../domain/layout.md; enum LayoutSchemaKind —
../domain/reference-data.md.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
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-LAY-2) | — |
Rooms | body | short | Да | 0..20; 0 = студия | — |
TotalArea | body | decimal | Да | > 0, precision (8,2), м² | — |
LivingArea | body | decimal? | Нет | > 0 и <= TotalArea, precision (8,2), м² | — |
Schemas | body | LayoutSchemasDocument? | Нет (default: пустой Items, ContractVersion = 1) | typed-модель (домен планировки): Items ≤ 20; элемент LayoutSchemaModel: FileId (Guid, storage/CDN), Kind enum Plan2D/Plan3D/FloorPosition/Visualization, Title? ≤ 128, SortOrder 0..100 | — |
LayoutGroupId | body | Guid? | Нет | существующая не архивная группа того же проекта (INV-LAY-3) | — |
Description | body | string? | Нет | <= 2000 симв. (varchar(2000)) | — |
PropertyObjectsCount, ExternalTalentLayoutId, SyncSource, ReplicaVersion, LastSyncedAt не
принимаются: денормализация считается фоново (project-aggregates-recalc), реплика-поля — только синк Talent.
Модель ответа CreateDeveloperLayoutResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutEntity.Id | — | Id созданной планировки. |
ProjectId | Guid | Да | ProjectId | — | Проект-владелец. |
LayoutGroupId | Guid? | Нет | LayoutGroupId | — | Группа планировок (если привязана). |
Version | long | Да | Version | — | 1. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
Денормализация PropertyObjectsCount = 0 наружу не возвращается — у новой планировки объектов недвижимости
ещё нет (пересчёт — background project-aggregates-recalc).
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manageв scope, покрывающемprojectId. При заголовкеIdempotency-Keyповтор возвращает сохранённыйIdбез повторной вставки и без повторного события; тот же ключ с иным телом →IDEMPOTENCY_KEY_MISMATCH(409). - Проверка входа и предусловий.
- формат/диапазоны полей (включая вложенную
Schemasи правилоLivingArea <= TotalArea) — иначеValidationError; - проект-родитель существует в пределах tenant-а и не архивирован — иначе
CHESSBOARD_PROJECT_NOT_FOUND(404; архивный/чужой проект неотличим от отсутствующего наружу); - при заданном
LayoutGroupId(INV-LAY-3) — группа существует, не архивна и того же проекта, иначеCHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH(409); - имя уникально в проекте среди не удалённых (INV-LAY-2) — иначе
CHESSBOARD_LAYOUT_NAME_TAKEN(409); гонка добивается unique indexux_layouts_project_name.
- формат/диапазоны полей (включая вложенную
- Создание планировки. Планировка заводится с
SyncSource = Pin;TenantIdберётся из проекта (INV-LAY-1),ProjectId— от проекта. Поля берутся из формы; пустыеSchemasинициализируются пустой typed-моделью (ContractVersion = 1); денормализацияPropertyObjectsCount = 0; проставляютсяCreatedByUserId,Version = 1. - Факты и события. Публикуется событие
pin.chessboard.layout-upserted.v1(снапшот без тяжёлого jsonbSchemas). При привязке к группе запрашивается фоновый пересчётLayoutGroup.LayoutsCount/Area. - Результат. Возвращаются
Id,ProjectId,LayoutGroupId,Version = 1,CreatedAt; HTTP201+Location. Маскирование не требуется (PII нет).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutUpsertedKafkaEvent → pin.chessboard.layout-upserted.v1, key = Id; потребители: dev-stats (измерение Layout), rewards (условия правил по Layout), invest-analytics (типы объектов недвижимости), catalog (проекция в карточке объекта недвижимости) |
| Проекции / пересчёт | PropertyObjectsCount уточняется фоново job-ом project-aggregates-recalc; при привязке к группе — пересчёт LayoutGroup.LayoutsCount/Area |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение ограничений полей / LivingArea > TotalArea / Schemas вне лимитов | Список ошибок полей | no retry |
CHESSBOARD_PROJECT_NOT_FOUND | 404 | Проект не найден / чужой tenant / архивный | «Проект не найден» | no retry |
CHESSBOARD_LAYOUT_NAME_TAKEN | 409 | Дубль имени в проекте (INV-LAY-2) | «Название планировки занято» | no retry |
CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH | 409 | LayoutGroupId не найден / архивный / из другого проекта (INV-LAY-3) | «Группа планировок принадлежит другому проекту» | no retry |
IDEMPOTENCY_KEY_MISMATCH | 409 | Тот же Idempotency-Key переиспользован с иным телом запроса | «Ключ идемпотентности переиспользован» | no retry |
FORBIDDEN | 403 | Нет chessboard.layouts.manage в scope | «Недостаточно прав» | no retry |
Сквозные UNAUTHORIZED / CHESSBOARD_CONCURRENCY_CONFLICT (unique-гонка INV-LAY-2 на уровне БД) — в
README.md.
Совместимость и наблюдаемость
- Новые optional поля формы — additive; изменение семантики
Schemas—ContractVersion + 1(домен планировки). Удаление/переименование/смена типа поля — breaking (migration + rollout). - Logs:
TenantId,ProjectId,LayoutId(после создания),LayoutGroupId(PII нет); metrics:chessboard_developers_layout_create_total{result}, latency p95; traces — OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- Happy path. Дано: менеджер застройщика с
chessboard.layouts.manageв scope проекта; проект tenant-а существует и не архивирован. Когда:POSTс валидным телом. Тогда: создана планировкаSyncSource = Pin,Version = 1,PropertyObjectsCount = 0,TenantIdунаследован от проекта (INV-LAY-1);201+Location; опубликованоpin.chessboard.layout-upserted.v1(снапшот без тяжёлогоSchemas). - Привязка к группе того же проекта. Дано:
LayoutGroupIdуказывает на живую группу того же проекта. Когда: создание. Тогда: планировка привязана, запрошен пересчётLayoutGroup.LayoutsCount/Area. - Группа чужого проекта/архивная. Дано:
LayoutGroupIdиз другого проекта либо архивная. Когда: создание. Тогда:CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH(409, INV-LAY-3). - Дубль имени. Дано: в проекте уже есть не удалённая планировка с тем же
Name. Когда: создание. Тогда:CHESSBOARD_LAYOUT_NAME_TAKEN(409, INV-LAY-2). - Границы полей и схем. Дано:
LivingArea > TotalArea, либоSchemas.Items > 20, либо элемент схемы с недопустимымKind/SortOrder. Когда: создание. Тогда:ValidationError(400). - Проект чужой/архивный. Дано:
projectIdдругого tenant-а либо архивирован. Когда: создание. Тогда:CHESSBOARD_PROJECT_NOT_FOUND(404, чужой/архивный неотличим от отсутствующего). - Идемпотентный повтор. Дано: первый вызов под
Idempotency-Key = K. Когда: повтор с тем жеKи телом → тот жеId; при ином теле →IDEMPOTENCY_KEY_MISMATCH(409). - Eventual consistency. После
201планировка доступна для привязки объектов недвижимости сразу; измерение Layout вdev-stats, правилаrewards, типыinvest-analyticsи проекция карточкиcatalogпоявляются после потребления события (окно — SLO событий).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId}/layouts — Список планировок проекта, POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки, PUT /api/v1/developers/layouts/{layoutId} — Обновление планировки
- События (1): Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/layout.md (INV-LAY-1/2/3,
LayoutSchemasDocument); группа: ../domain/layout-group.md; родитель: ../domain/project.md; enum схем: ../domain/reference-data.md (LayoutSchemaKind). - Событие: ../events/layout-upserted.md
(
pin.chessboard.layout-upserted.v1). - Соседние операции планировки: list-layouts.md, update-layout.md, archive-layout.md; группы — create-layout-group.md; реестр — README.md.