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

POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки

Назначение

Менеджер застройщика заводит типовую планировку проекта своей компании (SyncSource = Pin): комнатность, площади, схемы (2D/3D-план, положение на этаже, визуализации) и опциональную привязку к группе планировок. На планировку далее ссылаются объекты недвижимости (PropertyObject.LayoutId), правила вознаграждений (rewards) и эконом-модель по типам объектов (invest-analytics); dev-stats использует её как уровень фильтрации статистики (ADR-0057). Дом полей — сущность Планировка.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/projects/{projectId}/layouts
Success status201 (Location: /api/v1/developers/layouts/{id})
Idempotency headerIdempotency-Key (опционально; повтор с тем же ключом возвращает первый результат)
Correlationсквозной trace через заголовок traceparent

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

Ограничения полей — из дома ../domain/layout.md; enum LayoutSchemaKind../domain/reference-data.md.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ProjectIdrouteGuidДасуществующий проект tenant-а, не Archived404 при чужом
NamebodystringДа1..128 (trim); уникально в проекте среди не удалённых (INV-LAY-2)
RoomsbodyshortДа0..20; 0 = студия
TotalAreabodydecimalДа> 0, precision (8,2), м²
LivingAreabodydecimal?Нет> 0 и <= TotalArea, precision (8,2), м²
SchemasbodyLayoutSchemasDocument?Нет (default: пустой Items, ContractVersion = 1)typed-модель (домен планировки): Items ≤ 20; элемент LayoutSchemaModel: FileId (Guid, storage/CDN), Kind enum Plan2D/Plan3D/FloorPosition/Visualization, Title? ≤ 128, SortOrder 0..100
LayoutGroupIdbodyGuid?Нетсуществующая не архивная группа того же проекта (INV-LAY-3)
Descriptionbodystring?Нет<= 2000 симв. (varchar(2000))
Поля, не принимаемые в запросе

PropertyObjectsCount, ExternalTalentLayoutId, SyncSource, ReplicaVersion, LastSyncedAt не принимаются: денормализация считается фоново (project-aggregates-recalc), реплика-поля — только синк Talent.

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаLayoutEntity.IdId созданной планировки.
ProjectIdGuidДаProjectIdПроект-владелец.
LayoutGroupIdGuid?НетLayoutGroupIdГруппа планировок (если привязана).
VersionlongДаVersion1.
CreatedAtDateTimeOffsetДаCreatedAtМомент создания.

Денормализация PropertyObjectsCount = 0 наружу не возвращается — у новой планировки объектов недвижимости ещё нет (пересчёт — background project-aggregates-recalc).

Алгоритм

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

Диаграмма

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

ТипДетали
Интеграционные событияLayoutUpsertedKafkaEventpin.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
ValidationError400Нарушение ограничений полей / LivingArea > TotalArea / Schemas вне лимитовСписок ошибок полейno retry
CHESSBOARD_PROJECT_NOT_FOUND404Проект не найден / чужой tenant / архивный«Проект не найден»no retry
CHESSBOARD_LAYOUT_NAME_TAKEN409Дубль имени в проекте (INV-LAY-2)«Название планировки занято»no retry
CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH409LayoutGroupId не найден / архивный / из другого проекта (INV-LAY-3)«Группа планировок принадлежит другому проекту»no retry
IDEMPOTENCY_KEY_MISMATCH409Тот же Idempotency-Key переиспользован с иным телом запроса«Ключ идемпотентности переиспользован»no retry
FORBIDDEN403Нет chessboard.layouts.manage в scope«Недостаточно прав»no retry

Сквозные UNAUTHORIZED / CHESSBOARD_CONCURRENCY_CONFLICT (unique-гонка INV-LAY-2 на уровне БД) — в README.md.

Совместимость и наблюдаемость

  • Новые optional поля формы — additive; изменение семантики SchemasContractVersion + 1 (домен планировки). Удаление/переименование/смена типа поля — breaking (migration + rollout).
  • Logs: TenantId, ProjectId, LayoutId (после создания), LayoutGroupId (PII нет); metrics: chessboard_developers_layout_create_total{result}, latency p95; traces — OTel (traceparent); dashboard chessboard-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 событий).

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

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

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