PUT /api/v1/developers/layouts/{layoutId} — Обновление планировки
Назначение
Менеджер застройщика редактирует типовую планировку своего проекта (полная замена редактируемых полей).
Реплика-поля Talent при SyncSource = TalentChessboard через эту операцию менять нельзя (INV-LAY-6
SyncOwnedFields); привязка к группе допускается только к группе того же проекта (INV-LAY-3). Дом полей —
сущность Планировка.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/update-layout.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.layouts.manage |
| Scope (RBAC) | организация / группа проектов / проект (проект планировки в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; чужая планировка → 404 (существование не раскрывается) |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/developers/layouts/{layoutId} |
| Success status | 200 |
| Idempotency header | не применяется — PUT семантически идемпотентен (full replace + Version) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Ограничения полей — дом ../domain/layout.md; Schemas — typed jsonb
LayoutSchemasDocument, Rooms-семантика (0 = студия) там же. Реплика-поля перечислены в INV-LAY-6.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
LayoutId | route | Guid | Да | существующая планировка tenant-а | 404 при чужой |
Name | body | string | Да | 1..128; уникально в проекте (INV-LAY-2); реплика-поле (INV-LAY-6) | — |
Rooms | body | short | Да | 0..20 (0 = студия); реплика-поле | — |
TotalArea | body | decimal | Да | numeric(8,2), > 0; реплика-поле (layout.square) | — |
LivingArea | body | decimal? | Нет | numeric(8,2), > 0 и <= TotalArea; реплика-поле (layout.livingSquare) | — |
Schemas | body | LayoutSchemasDocument | Да (дефолт пустой) | typed; Items ≤ 20, Kind ∈ LayoutSchemaKind, SortOrder 0..100; реплика-поле (layout.schemas) | — |
LayoutGroupId | body | Guid? | Нет | группа того же проекта (INV-LAY-3); реплика-поле привязки (layout.group) | — |
Description | body | string? | Нет | <= 2000 (PIN-поле — всегда редактируемо) | — |
Version | body | long | Да | >= 1; optimistic-сверка с LayoutEntity.Version | — |
Пока SyncSource = TalentChessboard, поля Name, Rooms, TotalArea, LivingArea, Schemas,
LayoutGroupId управляются синхронизацией Talent — правке через этот метод не подлежат, их значения в
запросе должны совпадать с текущими, иначе CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED (409). Редактируемо
всегда — PIN-поле Description.
ProjectId планировки через update не меняется (перенос между проектами не поддерживается);
PropertyObjectsCount — денормализованный счётчик (background project-aggregates-recalc), в тело не входит.
Модель ответа UpdateDeveloperLayoutResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutEntity.Id | — | Id планировки. |
Version | long | Да | Version (после инкремента) | — | Новая доменная версия (для следующего PUT). |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент сохранения. |
Полный состав планировки перечитывается через list-layouts.md.
Предусловия и постусловия
Предусловия:
- Актор авторизован и имеет
chessboard.layouts.manageв scope проекта планировки. - Планировка существует в tenant актора и не архивна.
- Клиент располагает актуальным
Version. - Целевая группа (
LayoutGroupId, если задана) принадлежит тому же проекту (INV-LAY-3). - Реплика-поля Talent редактируемы только при
SyncSource = Pin.
Постусловия при успехе:
- Обновлён набор полей, разрешённый
SyncSource(приTalentChessboard— толькоDescription; приPin— плюс имя, комнатность, площади, схемы, привязка группы);Version+1,UpdatedByUserId/UpdatedAtпроставлены; переходов статуса нет. - Инварианты уникальности имени в проекте (INV-LAY-2) и «группа того же проекта» (INV-LAY-3) не нарушены.
- В том же согласованном изменении поставлено на публикацию
pin.chessboard.layout-upserted.v1с полным снапшотом; при сменеTotalArea/LayoutGroupIdдополнительно запрошен фоновый пересчёт агрегатов группы.
Постусловие при отказе: планировка не изменена, событие не публикуется, Version сохранён.
Интеграционная последовательность
- Синхронно возвращается
200+Version/UpdatedAt; синхронных вызовов соседей нет. - Асинхронно
pin.chessboard.layout-upserted.v1(партиционный ключIdпланировки, порядок per-layout гарантирован) потребляют:dev-stats(измерение Layout),rewards(условия правил),invest-analytics(типы объектов недвижимости),catalog. Потребители применяют снапшоты монотонно поVersion. - Событийная цепочка пересчёта: при смене
TotalArea/LayoutGroupIdоперация запрашиваетproject-aggregates-recalc(debounce 30 с) → job пересчитываетLayoutGroup.Area/LayoutsCount→ публикует обновлённый снапшот группыpin.chessboard.layout-group-upserted.v1. Это следствие-событие приходит потребителям позже основного (окно debounce), поэтому агрегаты группы догоняют с задержкой.
Изменения планировки на витрине/в отчётах видны с лагом consumer; агрегаты группы — с лагом debounce-джоба. Компенсаций не требуется — снапшоты полные и идемпотентны.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manage. - Проверка входа и предусловий.
- формат/диапазоны полей — иначе
ValidationError; - планировка существует в пределах tenant-а и не архивна — иначе
CHESSBOARD_LAYOUT_NOT_FOUND(404); - sync-guard (INV-LAY-6): при
SyncSource = TalentChessboardлюбое отличие входных реплика-полей (Name,Rooms,TotalArea,LivingArea,Schemas,LayoutGroupId) от текущих →CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED(409); для реплики редактируем толькоDescription; - при смене имени — оно уникально в проекте среди не удалённых (INV-LAY-2), иначе
CHESSBOARD_LAYOUT_NAME_TAKEN(409); - при смене непустого
LayoutGroupId— группа того же проекта (INV-LAY-3), иначеCHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH(409); Versionсовпадает с текущей — иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- формат/диапазоны полей — иначе
- Применение правок. Набор мутируемых полей зависит от
SyncSource(INV-LAY-6): всегда — описание; приSyncSource = Pinдополнительно — имя, комнатность, площади, схемы (ContractVersion = 1), привязка группы. ПроставляютсяUpdatedByUserId, инкрементVersion. Переходов статуса нет. - Факты и события. Публикуется событие
pin.chessboard.layout-upserted.v1— полный снапшот после правки. При сменеTotalArea/LayoutGroupIdзапрашивается фоновый пересчётLayoutGroup.Area/LayoutsCount(project-aggregates-recalc, debounce 30 с). - Результат. Возвращаются
Id,Version,UpdatedAt; HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutUpsertedKafkaEvent → pin.chessboard.layout-upserted.v1, key = LayoutId; потребители: dev-stats (измерение Layout), rewards (условия правил), invest-analytics (типы объектов недвижимости), catalog |
| Проекции / пересчёт | при смене TotalArea/LayoutGroupId — фоновый пересчёт агрегатов группы (project-aggregates-recalc) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение format/min/max/enum полей | Список ошибок полей | no retry |
CHESSBOARD_LAYOUT_NOT_FOUND | 404 | Нет планировки / чужой tenant / архив | «Планировка не найдена» | no retry |
CHESSBOARD_LAYOUT_NAME_TAKEN | 409 | Дубль имени в проекте (INV-LAY-2) | «Название планировки занято» | no retry |
CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED | 409 | Правка реплика-поля Talent-реплики (INV-LAY-6) | «Поле управляется синхронизацией Talent» | no retry |
CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH | 409 | Группа не найдена/из другого проекта (INV-LAY-3) | «Группа планировок другого проекта» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет permission chessboard.layouts.manage | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional поля тела — additive;
Schemas— правилаContractVersionдокументаLayoutSchemasDocument(../domain/layout.md); удаление/переименование — breaking (migration + rollout). - Logs:
TenantId,LayoutId,ProjectId, изменённые поля (имена, без значений); metrics:chessboard_developers_layout_update_total{result}; traces: OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- AC1 (правка PIN-полей и реплики при
Pin). Дано: планировкаSyncSource = Pin, актуальныйVersion. Когда: меняются имя, площади, схемы. Тогда: поля обновлены,Version+1, публикуетсяpin.chessboard.layout-upserted.v1с новым снапшотом. - AC2 (sync-guard реплики). Дано:
SyncSource = TalentChessboard. Когда: любое реплика-поле (Name/Rooms/TotalArea/LivingArea/Schemas/LayoutGroupId) отличается от текущего. Тогда:CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED(409); правка толькоDescriptionпроходит. - AC3 (дубль имени). Дано: имя занято другой не удалённой планировкой проекта. Когда: переименование.
Тогда:
CHESSBOARD_LAYOUT_NAME_TAKEN(409). - AC4 (группа другого проекта). Дано:
LayoutGroupIdиз другого проекта или несуществующая. Когда: привязка. Тогда:CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH(409). - AC5 (пересчёт агрегатов группы). Дано: планировка привязана к группе. Когда: меняется
TotalAreaилиLayoutGroupId. Тогда: запрошенproject-aggregates-recalc;LayoutGroup.Area/LayoutsCountсходятся после окна debounce и приходят потребителям следующим снапшотом группы. - AC6 (конкурентная правка). Дано: устаревший
Version. Когда: сохранение. Тогда:CHESSBOARD_CONCURRENCY_CONFLICT(409); проигравший перечитывает и повторяет. - AC7 (архивная/чужая планировка). Дано: планировка архивна или чужого tenant. Когда: операция. Тогда:
CHESSBOARD_LAYOUT_NOT_FOUND(404), событие не публикуется.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId}/layouts — Список планировок проекта, POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки
- События (1): Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/layout.md (INV-LAY-2/3/6); группа:
../domain/layout-group.md; enum-ы (
LayoutSchemaKind,ChessboardSyncSource): ../domain/reference-data.md. - Соседи по группе: create-layout.md, list-layouts.md, archive-layout.md; реестр: README.md.
- Событие: ../events/layout-upserted.md.