POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки
Назначение
Менеджер застройщика удаляет ставшую ненужной типовую планировку из проекта. Физического удаления нет —
мягкое удаление (soft-delete): планировка выводится из активного ассортимента и служебных выборок, но строка
сохраняется, потому что на неё логически ссылаются объекты недвижимости (PropertyObject.LayoutId), правила
rewards и измерение dev-stats (ADR-0057). Архив разрешён только когда у планировки нет живых объектов
недвижимости (INV-LAY-5 ArchiveGuard). Обратная к созданию. Дом полей и инварианты —
сущность Планировка.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | layouts |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/archive-layout.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/layouts/{layoutId}/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 |
LayoutId | route | Guid | Да | != Guid.Empty; существующая живая планировка tenant-а | 404 при чужой/отсутствии |
Version | body | long? | Нет | если задан — >= 1, optimistic-сверка с LayoutEntity.Version | защита от гонки правки застройщика |
Тело — опциональный { version } для оптимистичной блокировки; причина архивации не хранится (поля
Reason в доме нет, ../domain/layout.md).
Модель ответа ArchiveDeveloperLayoutResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutEntity.Id | — | Id планировки. |
IsArchived | bool | Да | computed (DeletedAt != null) | — | Всегда true после успеха. |
Version | long | Да | LayoutEntity.Version | — | Новая версия агрегата. |
ArchivedAt | DateTimeOffset | Да | LayoutEntity.DeletedAt | — | Момент архивации (при повторе — момент первичного soft-delete). |
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.layouts.manageв scope проекта планировки. Если планировка уже архивирована (DeletedAt != null) — идемпотентный no-op: возвращается текущее состояние без изменений и без повторной публикации события. - Проверка входа и предусловий.
LayoutId != Guid.Empty,Version(если задан)>= 1— иначеValidationError;- планировка существует в пределах tenant-а — иначе
CHESSBOARD_LAYOUT_NOT_FOUND(404, чужой tenant неотличим от отсутствия); - при заданном
Version— совпадение с текущей версией, иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- Проверка занятости (INV-LAY-5 ArchiveGuard). У планировки не должно быть живых объектов недвижимости
(не удалённых объектов с
LayoutId = Id); при наличии →CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS(409). Строка планировки на время проверки удерживается пессимистичной блокировкой, чтобы конкурентное создание/перепривязка объекта недвижимости (валидирующего INV-CHS-007 через эту планировку) не проскочило между проверкой и записью. - Архивация. Планировка мягко удаляется:
DeletedAt = now,DeletedByUserId = UserId,UpdatedByUserId = UserId, инкрементVersion; прочие поля (имя, комнатность, площади, схемы, реплика-поля Talent) не трогаются (INV-LAY-6). Статусной state-машины нет — «архив» =DeletedAt. - Факты и события. Публикуется событие
pin.chessboard.layout-upserted.v1со снапшотомIsArchived = true(отдельного archive-топика нет — потребители читаютIsArchivedиз снапшота). Запрашивается фоновый пересчётLayoutGroup.LayoutsCountгруппы планировки (project-aggregates-recalc). - Результат. Возвращаются
Id,IsArchived = true,Version,ArchivedAt; HTTP200. Маскирование не требуется.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | LayoutUpsertedKafkaEvent (IsArchived = true) → pin.chessboard.layout-upserted.v1, key = Id; потребители: dev-stats (закрытие измерения Layout, ADR-0057), rewards (деактивация правил по Layout), invest-analytics (вывод типа объекта недвижимости из эконом-модели), catalog (снятие планировки из карточки объекта недвижимости) |
| Проекции / пересчёт | планировка выпадает из выборок (фильтр DeletedAt == null в query-слое); project-aggregates-recalc пересчитывает LayoutGroup.LayoutsCount группы планировки (debounce 30 c) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
CHESSBOARD_LAYOUT_NOT_FOUND | 404 | Планировки нет / чужой tenant | «Планировка не найдена» | no retry |
CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS | 409 | INV-LAY-5: существуют не удалённые объекты недвижимости с LayoutId = Id | «Нельзя архивировать планировку с объектами недвижимости — сначала отвяжите или удалите объекты недвижимости» | no retry (сначала освободить планировку) |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал (гонка правки/привязки объекта недвижимости) | «Данные изменены параллельно» | retry после перечитывания |
ValidationError | 400 | LayoutId == Guid.Empty / Version < 1 | Список ошибок полей | no retry |
FORBIDDEN | 403 | Нет chessboard.layouts.manage в scope | «Недостаточно прав» | no retry |
Уже архивированная планировка — не ошибка: идемпотентный 200. Гонка «создание/перепривязка объекта
недвижимости во время архивации» ограничена пессимистичной блокировкой строки планировки. UNAUTHORIZED
(401) и сквозные ошибки — по таблице сквозных ошибок области README.md.
Совместимость и наблюдаемость
- Метод — write, тело опциональное.
LayoutUpsertedKafkaEventпереиспользуется для create/update/archive — потребители различают архив по флагуIsArchived(additive-поле, не breaking). Восстановление из архива (Restore, снятиеDeletedAt) добавляется отдельным endpoint-ом при появлении требования — без breaking; сейчас архив терминален. Состав payload события — additive-only без bump; breaking →ContractVersion = 2- топик
.v2.
- топик
- Logs:
TenantId,ProjectId,LayoutId,hasActivePropertyObjects,archived/archive rejected (reason=has-property-objects); metrics:chessboard_developers_layout_archive_total{result}; traces — OTel (traceparent); dashboardchessboard-overview.
Acceptance Criteria
- Happy path. Дано: живая планировка tenant-а без живых объектов недвижимости. Когда:
POST …/archive. Тогда:DeletedAt = now,Versionувеличен, прочие поля не тронуты;200(IsArchived = true); опубликованоpin.chessboard.layout-upserted.v1(IsArchived = true); запрошен пересчётLayoutGroup.LayoutsCount. - Guard: есть объекты недвижимости. Дано: у планировки ≥1 не удалённый объект с
LayoutId = Id. Когда: архивация. Тогда:CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS(409, INV-LAY-5); архив не выполнен, событие не публикуется. - Идемпотентный повтор. Дано: планировка уже архивирована (
DeletedAt != null). Когда: повтор. Тогда:200no-op — без изменений и без повторного события. - Конкурентная привязка объекта. Дано: параллельно создаётся/перепривязывается объект на эту планировку.
Когда: архивация под пессимистичной блокировкой строки. Тогда: одна операция побеждает; если передан
устаревший
Version→CHESSBOARD_CONCURRENCY_CONFLICT(409). - Чужой tenant. Дано:
layoutIdдругого tenant-а. Когда: архивация. Тогда:CHESSBOARD_LAYOUT_NOT_FOUND(404, существование чужой записи не раскрывается). - Eventual consistency. После
200планировка выпадает из выборок сразу; закрытие измерения Layout вdev-stats, деактивация правилrewards, вывод типа изinvest-analyticsи снятие с карточки вcatalogпроисходят после потребления событияIsArchived = true(окно — SLO событий).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки, PUT /api/v1/developers/layouts/{layoutId} — Обновление планировки
- События (1): Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность-дом: ../domain/layout.md (поля, INV-LAY-5 ArchiveGuard, «архив» =
DeletedAt, snapshotLayoutUpsertedKafkaEvent); объекты недвижимости-блокеры — ../domain/property-object.md (INV-CHS-007 LayoutSameProject,LayoutId); reference data — ../domain/reference-data.md. - Событие: ../events/layout-upserted.md
(
pin.chessboard.layout-upserted.v1,IsArchived = true). - Соседние операции планировок: create-layout.md, update-layout.md, list-layouts.md; архив группы — archive-layout-group.md.
- Реестр и сквозные ошибки области: README.md; обзор сервиса: ../README.md.
- ADR: ADR-0052 (tenancy
TenantId = CompanyId), ADR-0056 (simplicity-first), ADR-0057 (dev-stats потребляет измерения).