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

POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок

Назначение

Менеджер застройщика удаляет более не нужную группу планировок («Студии», «Люкс с видом на море»), предварительно перенеся/архивировав входящие планировки. Физического удаления нет — мягкое удаление (soft-delete): группа убирается из ассортимента и служебных выборок, но строка сохраняется. Группа — уровень иерархии между проектом и планировкой, на неё настроены правила rewards и агрегация dev-stats (ADR-0057), поэтому архив запрещён, пока в группе есть живые планировки (INV-LGR-3). Обратная к созданию. Дом полей — сущность Группа планировок.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/layout-groups/{layoutGroupId}/archive
Success status200; повтор на уже архивированной группе — идемпотентный 200 (no-op)
Idempotency headerне применяется (естественная идемпотентность по достигнутому состоянию DeletedAt != null)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredстемпится в DeletedByUserId/UpdatedByUserId
TenantIdidentity claims (tenant_id)GuidДаRequired, != Guid.EmptyTenant isolation
LayoutGroupIdrouteGuidДа!= Guid.Empty; существующая живая группа tenant-а404 при чужой/отсутствии
Versionbodylong?Нетесли задан — >= 1, optimistic-сверка с LayoutGroupEntity.Versionзащита от гонки правки застройщика
Тело запроса

Тело — опциональный { version } для оптимистичной блокировки; причина архивации не хранится (поля Reason в доме нет, ../domain/layout-group.md).

Модель ответа ArchiveDeveloperLayoutGroupResult

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаLayoutGroupEntity.IdId группы.
IsArchivedboolДаcomputed (DeletedAt != null)Всегда true после успеха.
VersionlongДаLayoutGroupEntity.VersionНовая версия агрегата.
ArchivedAtDateTimeOffsetДаLayoutGroupEntity.DeletedAtМомент архивации (при повторе — момент первичного soft-delete).

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.layouts.manage в scope проекта группы. Если группа уже архивирована (DeletedAt != null) — идемпотентный no-op: возвращается текущее состояние без изменений и без повторной публикации события.
  2. Проверка входа и предусловий.
    • LayoutGroupId != Guid.Empty, Version (если задан) >= 1 — иначе ValidationError;
    • группа существует в пределах tenant-а — иначе CHESSBOARD_LAYOUT_GROUP_NOT_FOUND (404);
    • при заданном Version — совпадение с текущей версией, иначе CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Проверка занятости (INV-LGR-3 ArchiveGuard). Группа должна быть пуста — без не архивированных планировок (Layout.LayoutGroupId = Id AND DeletedAt == null); при наличии → CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS (409). Строка группы на время проверки удерживается пессимистичной блокировкой, чтобы конкурентное создание/перепривязка планировки не проскочило между проверкой и записью.
  4. Архивация. Группа мягко удаляется: DeletedAt = now, DeletedByUserId = UserId, UpdatedByUserId = UserId, инкремент Version; прочие поля (имя, порядок, описание, денормализация) не трогаются. Статусной state-машины нет — «архив» = DeletedAt.
  5. Факты и события. Публикуется событие pin.chessboard.layout-group-upserted.v1 со снапшотом IsArchived = true (отдельного archive-топика нет — потребители читают IsArchived из снапшота).
  6. Результат. Возвращаются Id, IsArchived = true, Version, ArchivedAt; HTTP 200. Маскирование не требуется.

Диаграмма

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

ТипДетали
Интеграционные события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_FOUND404Группы нет / чужой tenant / уже физически отсутствует«Группа планировок не найдена»no retry
CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS409В группе есть не архивированные планировки (INV-LGR-3)«Нельзя архивировать группу с планировками — сначала перенесите или архивируйте планировки»no retry (сначала освободить группу)
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал (гонка правки/создания планировки)«Данные изменены параллельно»retry после перечитывания
ValidationError400LayoutGroupId == Guid.Empty / Version < 1Список ошибок полейno retry
FORBIDDEN403Нет chessboard.layouts.manage в scope«Недостаточно прав»no retry

Уже архивированная группа — не ошибка: идемпотентный 200. Гонка «создание/перепривязка планировки во время архивации» ограничена пессимистичной блокировкой группы. UNAUTHORIZED (401) и сквозные ошибки — по таблице сквозных ошибок области README.md.

Совместимость и наблюдаемость

  • Метод — write, тело опциональное. Восстановление из архива (Restore) при потребности добавляется отдельным endpoint-ом — без breaking change. Состав payload LayoutGroupUpsertedKafkaEvent — 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); dashboard chessboard-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); архив не выполнен, событие не публикуется.
  • Идемпотентный повтор. Дано: группа уже архивирована. Когда: повтор. Тогда: 200 no-op — без изменений и без повторного события; ArchivedAt = момент первичного soft-delete.
  • Конкурентное добавление планировки. Дано: параллельно создаётся/перепривязывается планировка в группу. Когда: архивация под пессимистичной блокировкой группы. Тогда: гонка «проверка пустоты vs вставка» исключена блокировкой; при устаревшем VersionCHESSBOARD_CONCURRENCY_CONFLICT (409).
  • Чужой tenant. Дано: layoutGroupId другого tenant-а. Когда: архивация. Тогда: CHESSBOARD_LAYOUT_GROUP_NOT_FOUND (404).
  • Eventual consistency. После 200 группа выпадает из выборок сразу; закрытие измерения LayoutGroup в dev-stats, деактивация условий правил rewards и снятие группы из ассортимента catalog — после потребления события (окно — SLO событий).

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

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

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