POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок
Назначение
Менеджер застройщика удаляет более не нужную группу планировок («Студии», «Люкс с видом на море»),
предварительно перенеся/архивировав входящие планировки. Физического удаления нет — мягкое удаление
(soft-delete): группа убирается из ассортимента и служебных выборок, но строка сохраняется. Группа — уровень
иерархии между проектом и планировкой, на неё настроены правила rewards и агрегация dev-stats
(ADR-0057), поэтому архив запрещён, пока в группе есть живые планировки (INV-LGR-3). Обратная к
созданию. Дом полей — сущность
Группа планировок.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/archive-layout-group.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.layouts.manage |
| Scope (RBAC) | организация / группа проектов / проект (проект группы в scope) |
| Tenant isolation | TenantId = CompanyId из JWT (ADR-0052); чужая группа → 404 (существование не раскрывается) |
| Audit | soft-delete: DeletedAt, DeletedByUserId, UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/layout-groups/{layoutGroupId}/archive |
| Success status | 200; повтор на уже архивированной группе — идемпотентный 200 (no-op) |
| Idempotency header | не применяется (естественная идемпотентность по достигнутому состоянию DeletedAt != null) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | стемпится в DeletedByUserId/UpdatedByUserId |
TenantId | identity claims (tenant_id) | Guid | Да | Required, != Guid.Empty | Tenant isolation |
LayoutGroupId | route | Guid | Да | != Guid.Empty; существующая живая группа tenant-а | 404 при чужой/отсутствии |
Version | body | long? | Нет | если задан — >= 1, optimistic-сверка с LayoutGroupEntity.Version | защита от гонки правки застройщика |
Тело — опциональный { version } для оптимистичной блокировки; причина архивации не хранится (поля
Reason в доме нет, ../domain/layout-group.md).
Модель ответа ArchiveDeveloperLayoutGroupResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutGroupEntity.Id | — | Id группы. |
IsArchived | bool | Да | computed (DeletedAt != null) | — | Всегда true после успеха. |
Version | long | Да | LayoutGroupEntity.Version | — | Новая версия агрегата. |
ArchivedAt | DateTimeOffset | Да | LayoutGroupEntity.DeletedAt | — | Момент архивации (при повторе — момент первичного soft-delete). |
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manageв scope проекта группы. Если группа уже архивирована (DeletedAt != null) — идемпотентный no-op: возвращается текущее состояние без изменений и без повторной публикации события. - Проверка входа и предусловий.
LayoutGroupId != Guid.Empty,Version(если задан)>= 1— иначеValidationError;- группа существует в пределах tenant-а — иначе
CHESSBOARD_LAYOUT_GROUP_NOT_FOUND(404); - при заданном
Version— совпадение с текущей версией, иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- Проверка занятости (INV-LGR-3 ArchiveGuard). Группа должна быть пуста — без не архивированных
планировок (
Layout.LayoutGroupId = Id AND DeletedAt == null); при наличии →CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS(409). Строка группы на время проверки удерживается пессимистичной блокировкой, чтобы конкурентное создание/перепривязка планировки не проскочило между проверкой и записью. - Архивация. Группа мягко удаляется:
DeletedAt = now,DeletedByUserId = UserId,UpdatedByUserId = UserId, инкрементVersion; прочие поля (имя, порядок, описание, денормализация) не трогаются. Статусной state-машины нет — «архив» =DeletedAt. - Факты и события. Публикуется событие
pin.chessboard.layout-group-upserted.v1со снапшотомIsArchived = true(отдельного archive-топика нет — потребители читаютIsArchivedиз снапшота). - Результат. Возвращаются
Id,IsArchived = true,Version,ArchivedAt; HTTP200. Маскирование не требуется.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutGroupUpsertedKafkaEvent (IsArchived = true) → pin.chessboard.layout-group-upserted.v1, key = Id; потребители: dev-stats (закрытие измерения LayoutGroup, ADR-0057), rewards (деактивация условий правил по группе), catalog (снятие группы из ассортимента) |
| Проекции / пересчёт | группа выпадает из выборок (фильтр DeletedAt == null в query-слое); пересчёт агрегатов проекта не требуется — группа была пуста (INV-LGR-3) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
CHESSBOARD_LAYOUT_GROUP_NOT_FOUND | 404 | Группы нет / чужой tenant / уже физически отсутствует | «Группа планировок не найдена» | no retry |
CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS | 409 | В группе есть не архивированные планировки (INV-LGR-3) | «Нельзя архивировать группу с планировками — сначала перенесите или архивируйте планировки» | no retry (сначала освободить группу) |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал (гонка правки/создания планировки) | «Данные изменены параллельно» | retry после перечитывания |
ValidationError | 400 | LayoutGroupId == Guid.Empty / Version < 1 | Список ошибок полей | no retry |
FORBIDDEN | 403 | Нет chessboard.layouts.manage в scope | «Недостаточно прав» | no retry |
Уже архивированная группа — не ошибка: идемпотентный 200. Гонка «создание/перепривязка планировки во
время архивации» ограничена пессимистичной блокировкой группы. UNAUTHORIZED (401) и сквозные ошибки — по
таблице сквозных ошибок области README.md.
Совместимость и наблюдаемость
- Метод — write, тело опциональное. Восстановление из архива (
Restore) при потребности добавляется отдельным endpoint-ом — без breaking change. Состав payloadLayoutGroupUpsertedKafkaEvent— additive-only без bump; breaking →ContractVersion = 2+ топик.v2. - Logs:
TenantId,ProjectId,LayoutGroupId,archived/archive rejected (reason=has-layouts); metrics:chessboard_developers_layout_group_archive_total{result}; traces — OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- Happy path. Дано: живая группа tenant-а без не архивированных планировок. Когда:
POST …/archive. Тогда:DeletedAt = now,Versionувеличен, прочие поля не тронуты;200(IsArchived = true); опубликованоpin.chessboard.layout-group-upserted.v1(IsArchived = true); пересчёт агрегатов не нужен (группа была пуста, INV-LGR-3). - Guard: есть планировки. Дано: в группе ≥1 не архивированная планировка (
LayoutGroupId = Id AND DeletedAt == null). Когда: архивация. Тогда:CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS(409, INV-LGR-3); архив не выполнен, событие не публикуется. - Идемпотентный повтор. Дано: группа уже архивирована. Когда: повтор. Тогда:
200no-op — без изменений и без повторного события;ArchivedAt= момент первичного soft-delete. - Конкурентное добавление планировки. Дано: параллельно создаётся/перепривязывается планировка в группу.
Когда: архивация под пессимистичной блокировкой группы. Тогда: гонка «проверка пустоты vs вставка»
исключена блокировкой; при устаревшем
Version→CHESSBOARD_CONCURRENCY_CONFLICT(409). - Чужой tenant. Дано:
layoutGroupIdдругого tenant-а. Когда: архивация. Тогда:CHESSBOARD_LAYOUT_GROUP_NOT_FOUND(404). - Eventual consistency. После
200группа выпадает из выборок сразу; закрытие измерения LayoutGroup вdev-stats, деактивация условий правилrewardsи снятие группы из ассортиментаcatalog— после потребления события (окно — SLO событий).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки, POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок, PUT /api/v1/developers/layout-groups/{layoutGroupId} — Обновление группы планировок
- События (1): Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность-дом: ../domain/layout-group.md (поля, INV-LGR-3 ArchiveGuard,
snapshot
LayoutGroupUpsertedKafkaEvent); дочерние планировки: ../domain/layout.md (INV-LAY-5, «архив» =DeletedAt). - Событие:
pin.chessboard.layout-group-upserted.v1— дом снапшота в ../domain/layout-group.md; контракт-файл ../events/layout-group-upserted.md. - Соседние операции группы: create-layout-group.md, update-layout-group.md, list-layout-groups.md; архив планировки — archive-layout.md.
- Реестр и сквозные ошибки области: README.md; обзор сервиса: ../README.md.
- ADR: ADR-0052 (tenancy
TenantId = CompanyId), ADR-0057 (dev-stats потребляет измерения).