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

PUT /api/v1/developers/layouts/{layoutId} — Обновление планировки

Назначение

Менеджер застройщика редактирует типовую планировку своего проекта (полная замена редактируемых полей). Реплика-поля Talent при SyncSource = TalentChessboard через эту операцию менять нельзя (INV-LAY-6 SyncOwnedFields); привязка к группе допускается только к группе того же проекта (INV-LAY-3). Дом полей — сущность Планировка.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (менеджер застройщика)
Auth policy / Permissionschessboard.layouts.manage
Scope (RBAC)организация / группа проектов / проект (проект планировки в scope)
Tenant isolationTenantId = CompanyId из JWT; чужая планировка → 404 (существование не раскрывается)
Auditобновление: UpdatedByUserId, инкремент Version

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/developers/layouts/{layoutId}
Success status200
Idempotency headerне применяется — PUT семантически идемпотентен (full replace + Version)
Correlationсквозной trace через заголовок traceparent

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

Ограничения полей — дом ../domain/layout.md; Schemas — typed jsonb LayoutSchemasDocument, Rooms-семантика (0 = студия) там же. Реплика-поля перечислены в INV-LAY-6.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
LayoutIdrouteGuidДасуществующая планировка tenant-а404 при чужой
NamebodystringДа1..128; уникально в проекте (INV-LAY-2); реплика-поле (INV-LAY-6)
RoomsbodyshortДа0..20 (0 = студия); реплика-поле
TotalAreabodydecimalДаnumeric(8,2), > 0; реплика-поле (layout.square)
LivingAreabodydecimal?Нетnumeric(8,2), > 0 и <= TotalArea; реплика-поле (layout.livingSquare)
SchemasbodyLayoutSchemasDocumentДа (дефолт пустой)typed; Items ≤ 20, KindLayoutSchemaKind, SortOrder 0..100; реплика-поле (layout.schemas)
LayoutGroupIdbodyGuid?Нетгруппа того же проекта (INV-LAY-3); реплика-поле привязки (layout.group)
Descriptionbodystring?Нет<= 2000 (PIN-поле — всегда редактируемо)
VersionbodylongДа>= 1; optimistic-сверка с LayoutEntity.Version
Sync-guard реплика-полей Talent (INV-LAY-6)

Пока 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Описание
IdGuidДаLayoutEntity.IdId планировки.
VersionlongДаVersion (после инкремента)Новая доменная версия (для следующего PUT).
UpdatedAtDateTimeOffsetДа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), поэтому агрегаты группы догоняют с задержкой.
Окно eventual-consistency

Изменения планировки на витрине/в отчётах видны с лагом consumer; агрегаты группы — с лагом debounce-джоба. Компенсаций не требуется — снапшоты полные и идемпотентны.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.layouts.manage.
  2. Проверка входа и предусловий.
    • формат/диапазоны полей — иначе 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).
  3. Применение правок. Набор мутируемых полей зависит от SyncSource (INV-LAY-6): всегда — описание; при SyncSource = Pin дополнительно — имя, комнатность, площади, схемы (ContractVersion = 1), привязка группы. Проставляются UpdatedByUserId, инкремент Version. Переходов статуса нет.
  4. Факты и события. Публикуется событие pin.chessboard.layout-upserted.v1 — полный снапшот после правки. При смене TotalArea/LayoutGroupId запрашивается фоновый пересчёт LayoutGroup.Area/LayoutsCount (project-aggregates-recalc, debounce 30 с).
  5. Результат. Возвращаются Id, Version, UpdatedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияLayoutUpsertedKafkaEventpin.chessboard.layout-upserted.v1, key = LayoutId; потребители: dev-stats (измерение Layout), rewards (условия правил), invest-analytics (типы объектов недвижимости), catalog
Проекции / пересчётпри смене TotalArea/LayoutGroupId — фоновый пересчёт агрегатов группы (project-aggregates-recalc)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нарушение format/min/max/enum полейСписок ошибок полейno retry
CHESSBOARD_LAYOUT_NOT_FOUND404Нет планировки / чужой tenant / архив«Планировка не найдена»no retry
CHESSBOARD_LAYOUT_NAME_TAKEN409Дубль имени в проекте (INV-LAY-2)«Название планировки занято»no retry
CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED409Правка реплика-поля Talent-реплики (INV-LAY-6)«Поле управляется синхронизацией Talent»no retry
CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH409Группа не найдена/из другого проекта (INV-LAY-3)«Группа планировок другого проекта»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет 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); dashboard chessboard-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), событие не публикуется.

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

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

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