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

POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки

Назначение

Менеджер застройщика удаляет ставшую ненужной типовую планировку из проекта. Физического удаления нет — мягкое удаление (soft-delete): планировка выводится из активного ассортимента и служебных выборок, но строка сохраняется, потому что на неё логически ссылаются объекты недвижимости (PropertyObject.LayoutId), правила rewards и измерение dev-stats (ADR-0057). Архив разрешён только когда у планировки нет живых объектов недвижимости (INV-LAY-5 ArchiveGuard). Обратная к созданию. Дом полей и инварианты — сущность Планировка.

Metadata

ПолеЗначение
Сервис/контекстchessboard
API areadevelopers
Feature grouplayouts
Статусdraft
Документdocs/06-services/chessboard/api/developers/archive-layout.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/layouts/{layoutId}/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
LayoutIdrouteGuidДа!= Guid.Empty; существующая живая планировка tenant-а404 при чужой/отсутствии
Versionbodylong?Нетесли задан — >= 1, optimistic-сверка с LayoutEntity.Versionзащита от гонки правки застройщика
Тело запроса

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

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

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.layouts.manage в scope проекта планировки. Если планировка уже архивирована (DeletedAt != null) — идемпотентный no-op: возвращается текущее состояние без изменений и без повторной публикации события.
  2. Проверка входа и предусловий.
    • LayoutId != Guid.Empty, Version (если задан) >= 1 — иначе ValidationError;
    • планировка существует в пределах tenant-а — иначе CHESSBOARD_LAYOUT_NOT_FOUND (404, чужой tenant неотличим от отсутствия);
    • при заданном Version — совпадение с текущей версией, иначе CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Проверка занятости (INV-LAY-5 ArchiveGuard). У планировки не должно быть живых объектов недвижимости (не удалённых объектов с LayoutId = Id); при наличии → CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS (409). Строка планировки на время проверки удерживается пессимистичной блокировкой, чтобы конкурентное создание/перепривязка объекта недвижимости (валидирующего INV-CHS-007 через эту планировку) не проскочило между проверкой и записью.
  4. Архивация. Планировка мягко удаляется: DeletedAt = now, DeletedByUserId = UserId, UpdatedByUserId = UserId, инкремент Version; прочие поля (имя, комнатность, площади, схемы, реплика-поля Talent) не трогаются (INV-LAY-6). Статусной state-машины нет — «архив» = DeletedAt.
  5. Факты и события. Публикуется событие pin.chessboard.layout-upserted.v1 со снапшотом IsArchived = true (отдельного archive-топика нет — потребители читают IsArchived из снапшота). Запрашивается фоновый пересчёт LayoutGroup.LayoutsCount группы планировки (project-aggregates-recalc).
  6. Результат. Возвращаются Id, IsArchived = true, Version, ArchivedAt; HTTP 200. Маскирование не требуется.

Диаграмма

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

ТипДетали
Интеграционные события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_FOUND404Планировки нет / чужой tenant«Планировка не найдена»no retry
CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS409INV-LAY-5: существуют не удалённые объекты недвижимости с LayoutId = Id«Нельзя архивировать планировку с объектами недвижимости — сначала отвяжите или удалите объекты недвижимости»no retry (сначала освободить планировку)
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал (гонка правки/привязки объекта недвижимости)«Данные изменены параллельно»retry после перечитывания
ValidationError400LayoutId == Guid.Empty / Version < 1Список ошибок полейno retry
FORBIDDEN403Нет 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); dashboard chessboard-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). Когда: повтор. Тогда: 200 no-op — без изменений и без повторного события.
  • Конкурентная привязка объекта. Дано: параллельно создаётся/перепривязывается объект на эту планировку. Когда: архивация под пессимистичной блокировкой строки. Тогда: одна операция побеждает; если передан устаревший VersionCHESSBOARD_CONCURRENCY_CONFLICT (409).
  • Чужой tenant. Дано: layoutId другого tenant-а. Когда: архивация. Тогда: CHESSBOARD_LAYOUT_NOT_FOUND (404, существование чужой записи не раскрывается).
  • Eventual consistency. После 200 планировка выпадает из выборок сразу; закрытие измерения Layout в dev-stats, деактивация правил rewards, вывод типа из invest-analytics и снятие с карточки в catalog происходят после потребления события IsArchived = true (окно — SLO событий).

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

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

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