PUT /api/v1/developers/layout-groups/{layoutGroupId} — Обновление группы планировок
Назначение
Менеджер застройщика правит редактируемые поля группы планировок своей компании: Name, Description,
SortOrder (переименование, описание, порядок отображения в ассортименте). Дом полей — сущность
Группа планировок.
При SyncSource = TalentChessboard поле Name управляется синхронизацией Talent и через эту операцию не
меняется (INV-LGR-2 в реплике поддерживает потребитель синка).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/update-layout-group.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.layouts.manage |
| Scope (RBAC) | организация / группа проектов / проект (group.ProjectId в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; чужая группа → 404 (существование не раскрывается) |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/developers/layout-groups/{layoutGroupId} |
| Success status | 200 |
| Idempotency header | не применяется — PUT семантически идемпотентен (full replace + Version) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Ограничения полей — из дома ../domain/layout-group.md; ProjectId,
TenantId и SyncSource не принимаются (наследуются от загруженной группы / управляются синком).
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
LayoutGroupId | route | Guid | Да | существующая группа tenant-а, не архивная | 404 при чужой/отсутствии |
Name | body | string | Да | 1..128 (trim); уникально в проекте среди не удалённых (INV-LGR-2). Реплика-поле — при SyncSource = TalentChessboard изменение игнорируется (управляется синком) | — |
Description | body | string? | Нет | <= 2000 симв. (PIN-поле) | — |
SortOrder | body | int | Да | 0..1000 (PIN-поле; порядок отображения групп) | — |
Version | body | long | Да | >= 1; сверка с LayoutGroupEntity.Version (optimistic) | — |
LayoutsCount, Area не принимаются — денормализация считается фоново (project-aggregates-recalc);
ExternalTalentLayoutGroupId, ReplicaVersion, LastSyncedAt — только синк.
Модель ответа UpdateDeveloperLayoutGroupResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutGroupEntity.Id | — | Id группы. |
ProjectId | Guid | Да | LayoutGroupEntity.ProjectId | — | Проект-владелец. |
Version | long | Да | Version (после инкремента) | — | Новая доменная версия. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент сохранения. |
Полный список групп перечитывается через list-layout-groups.md.
Предусловия и постусловия
Предусловия:
- Актор авторизован и имеет
chessboard.layouts.manageв scope проекта группы. - Группа существует в tenant актора и не архивна.
- Клиент располагает актуальным
Versionгруппы. - Имя (
Name) — реплика-поле: реально меняется только приSyncSource = Pin; приTalentChessboardвходNameтрактуется как no-op.
Постусловия при успехе:
- Обновлены разрешённые
SyncSourceполя (всегда —Description,SortOrder; приPinдополнительно —Name);Version+1, проставленыUpdatedByUserId/UpdatedAt; статус не меняется. - Инвариант уникальности имени группы в проекте (INV-LGR-2) не нарушен.
- Денормализованные
LayoutsCount/Areaэтой операцией не трогаются — их уточняет фоновый пересчёт. - В том же согласованном изменении поставлено на публикацию
pin.chessboard.layout-group-upserted.v1.
Постусловие при отказе: группа не изменена, событие не публикуется, Version сохранён.
Интеграционная последовательность
- Синхронно возвращается
200+Id/ProjectId/Version/UpdatedAt; синхронных вызовов соседей нет. - Асинхронно
pin.chessboard.layout-group-upserted.v1(партиционный ключIdгруппы, порядок per-group гарантирован) потребляют:dev-stats(измерение LayoutGroup, ADR-0057),rewards(условия правил по LayoutGroup),catalog(группировка ассортимента). Потребители применяют снапшоты монотонно поVersion. - Отдельный фоновый канал:
LayoutsCount/Areaпересчитывает jobproject-aggregates-recalc— его результат приходит потребителям следующим снапшотом группы, а не этой правкой (окно debounce).
Переименование/переупорядочивание в ассортименте catalog и в отчётах dev-stats/rewards видно с лагом
consumer. Компенсаций не требуется — снапшот полный, идемпотентен.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manageв scope проекта группы. - Проверка входа и предусловий.
Name1..128trim,Description <= 2000,SortOrder 0..1000,Version >= 1— иначеValidationError;- группа существует в пределах tenant-а и не архивна — иначе
CHESSBOARD_LAYOUT_GROUP_NOT_FOUND(404, архивная неотличима от отсутствующей наружу); Versionсовпадает с текущей — иначеCHESSBOARD_CONCURRENCY_CONFLICT(409);- при
SyncSource = Pinи изменении имени — оно уникально в проекте среди не удалённых (INV-LGR-2), иначеCHESSBOARD_LAYOUT_GROUP_NAME_TAKEN(409); приSyncSource = TalentChessboardвходName— не ошибка, а no-op (поле управляется синком, проверка уникальности не выполняется).
- Применение правок. Набор мутируемых полей зависит от
SyncSource: всегда — описание, порядок (SortOrder); приSyncSource = Pinдополнительно — имя. ПроставляютсяUpdatedByUserId, инкрементVersion. Переходов статуса нет. - Факты и события. Публикуется событие
pin.chessboard.layout-group-upserted.v1— снапшот после правки (состав — layout-group.md «События»). - Результат. Возвращаются
Id,ProjectId,Version,UpdatedAt; HTTP200. Маскирование не требуется (PII нет).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutGroupUpsertedKafkaEvent → pin.chessboard.layout-group-upserted.v1, key = Id; потребители: dev-stats (измерение LayoutGroup, ADR-0057), rewards (условия правил по LayoutGroup), catalog (группировка ассортимента) |
| Проекции / пересчёт | LayoutsCount/Area уточняются фоново job-ом project-aggregates-recalc (эта операция их не трогает) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение ограничений полей (Name/Description/SortOrder/Version) | Список ошибок полей | no retry |
CHESSBOARD_LAYOUT_GROUP_NOT_FOUND | 404 | Нет группы / чужой tenant / вне scope / архивная | «Группа планировок не найдена» | no retry |
CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN | 409 | Дубль имени в проекте (INV-LGR-2) | «Название группы планировок занято» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Несовпадение Version / параллельная правка | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет chessboard.layouts.manage в scope | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional поля формы — additive; удаление/переименование/смена типа — breaking (migration + rollout);
изменение snapshot события — по правилам
ContractVersion(layout-group.md «События»). - Logs:
TenantId,ProjectId,LayoutGroupId, изменённые поля (имена, без значений); metrics:chessboard_developers_layout_group_update_total{result}; traces: OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- AC1 (правка PIN-полей). Дано: группа с актуальным
Version. Когда: меняютсяDescription/SortOrder. Тогда: поля обновлены,Version+1, публикуетсяpin.chessboard.layout-group-upserted.v1; порядок отображения групп в ассортименте меняется после обработки события. - AC2 (переименование при
Pin). Дано:SyncSource = Pin, новое имя свободно в проекте. Когда: переименование. Тогда:Nameобновлён, событие несёт новое имя. - AC3 (имя под синком — no-op). Дано:
SyncSource = TalentChessboard. Когда: входнойNameотличается. Тогда:Nameне меняется (без ошибки), проверка уникальности не выполняется;Description/SortOrderприменяются. - AC4 (дубль имени). Дано:
SyncSource = Pin, имя занято другой не удалённой группой проекта. Когда: переименование. Тогда:CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN(409). - AC5 (конкурентная правка). Дано: устаревший
Version. Когда: сохранение. Тогда:CHESSBOARD_CONCURRENCY_CONFLICT(409); проигравший перечитывает и повторяет. - AC6 (архивная/чужая группа). Дано: группа архивна, чужого tenant или вне scope. Когда: операция.
Тогда:
CHESSBOARD_LAYOUT_GROUP_NOT_FOUND(404), событие не публикуется.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок
- События (1): Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/layout-group.md (INV-LGR-2,
SyncSource, событиеLayoutGroupUpsertedKafkaEvent); enumChessboardSyncSource— ../domain/reference-data.md. - Создание: create-layout-group.md; архив: archive-layout-group.md; список: list-layout-groups.md; реестр: README.md.