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

POST /api/v1/developers/projects/{projectId}/unpublish — Снятие проекта с публикации

Назначение

Менеджер застройщика снимает проект с витрины: на доработку (Published → Draft) либо временно скрывает (Published → Hidden) — целевой статус задаётся параметром TargetStatus. Проект исчезает из публичной ленты; объекты недвижимости и здания сохраняются. Дом полей и жизненный цикл — сущность Проект.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/projects/{projectId}/unpublish
Success status200
Idempotency headerне применяется — повтор с тем же TargetStatus идемпотентен (no-op)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ProjectIdrouteGuidДасуществующий проект tenant-а404 при чужом
TargetStatusbodyProjectStatusДатолько Draft (Unpublish) или Hidden (Hide); прочие значения → ValidationError
VersionbodylongДа>= 1; optimistic-сверка

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаProjectEntity.IdId проекта.
StatusProjectStatusДаStatusDraft или Hidden.
VersionlongДаVersionНовая версия.
UpdatedAtDateTimeOffsetДаUpdatedAtМомент перехода.

Предусловия и постусловия

Предусловия:

  • Актор авторизован и имеет chessboard.projects.manage в scope, покрывающем проект.
  • Проект существует в tenant актора и находится в статусе Published — единственном, из которого допустим переход Draft (Unpublish) или Hidden (Hide).
  • Клиент располагает актуальным Version (получен из карточки/списка проектов).

Постусловия при успехе:

  • Project.Status = TargetStatus (Draft либо Hidden), Version увеличен на 1, проставлены UpdatedByUserId и UpdatedAt.
  • Здания и объекты недвижимости проекта не изменены — их собственные статусы и поля сохранены; скрытие с витрины работает только на уровне публичных проекций (каскад запросов), а не мутацией дочерних сущностей.
  • В том же согласованном изменении зафиксирован факт перехода и поставлено на публикацию интеграционное событие pin.chessboard.project-upserted.v1 со снапшотом нового Status.
  • Публичные read-операции самого chessboard немедленно отдают 404 (читают source of truth по Status = Published); распространение в catalog, поиск, favorites/selections — асинхронное (см. «Интеграционная последовательность»).

Постусловие при отказе: состояние проекта не изменилось, событие не публикуется, Version сохранён.

Идемпотентность

Если проект уже находится в TargetStatus — операция идемпотентна: 200 с текущим состоянием, Version не меняется, повторное событие не публикуется.

Интеграционная последовательность

  • Синхронно клиенту возвращается только результат перехода (200 + новый Status/Version); никаких синхронных вызовов соседних сервисов операция не делает.
  • Асинхронно событие pin.chessboard.project-upserted.v1 (партиционный ключ ProjectId, порядок событий по проекту гарантирован) потребляют: catalog — убирает проект из ленты/шахматки; поиск — переводит ProjectSearchDocument в tombstone (ADR-0059); favorites/selections — через каскад catalog-проекций помечают недоступность. Потребители применяют снапшоты монотонно по Version (снапшот с меньшим Version игнорируется), поэтому переупорядочивание/дубли доставки безопасны.
  • Окно eventual-consistency: между ответом и обработкой события пользователь может ещё видеть проект в ленте/поиске catalog; сам chessboard уже скрыл его. Окно ограничено лагом consumer-ов (цель — секунды, SLO README). Компенсаций/саги не требуется: событие несёт полный снапшот, повторная публикация того же состояния идемпотентна на стороне потребителя.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.projects.manage. Если статус уже равен TargetStatus — идемпотентный no-op (без повторной публикации события).
  2. Проверка входа и предусловий.
    • TargetStatus ∈ {Draft, Hidden}, Version >= 1 — иначе ValidationError;
    • проект существует в пределах tenant-а — иначе CHESSBOARD_PROJECT_NOT_FOUND (404);
    • переход допустим: Published → Draft (Unpublish) или Published → Hidden (Hide); текущий статус ∉ {Published, TargetStatus} → CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION (409);
    • Version совпадает с текущей — иначе CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Переход статуса. Проект переводится Published → Draft либо Published → Hidden по TargetStatus. Меняется только Status; объекты недвижимости и здания не мутируются (скрытие публичных проекций — каскадно в query-слое). Проставляются UpdatedByUserId, инкремент Version.
  4. Факты и события. Публикуется событие pin.chessboard.project-upserted.v1 — снапшот с новым Status.
  5. Результат. Возвращаются Id, Status, Version, UpdatedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияProjectUpsertedKafkaEvent (новый Status) → pin.chessboard.project-upserted.v1; catalog убирает проект из ленты/шахматки, favorites/selections помечают недоступность через каскад catalog-проекций
Проекции / поискпроект удаляется из Elasticsearch-проекции ProjectSearchDocument (tombstone при Status != Published, ../../search/projects.md, ADR-0059) и выпадает из публичной поисковой выдачи

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400TargetStatus ∉ {Draft, Hidden}Список ошибок полейno retry
CHESSBOARD_PROJECT_NOT_FOUND404Нет проекта / чужой tenant«Проект не найден»no retry
CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION409Текущий статус не Published«Недопустимый переход статуса»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет permission«Недостаточно прав»no retry

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

  • Контракт стабилен; Archive — отдельная будущая операция (INV-PRJ-8: запрет при активных холдах).
  • Logs: TenantId, ProjectId, Published → {TargetStatus}; metrics: chessboard_project_status_transition_total{from,to}.

Acceptance Criteria

  • AC1 (Unpublish на доработку). Дано: published-проект и актуальный Version. Когда: менеджер вызывает операцию с TargetStatus = Draft. Тогда: Status = Draft, Version +1, публикуется pin.chessboard.project-upserted.v1 со Status = Draft, публичная карточка проекта отдаёт 404.
  • AC2 (Hide — временное скрытие). Дано: published-проект. Когда: TargetStatus = Hidden. Тогда: Status = Hidden, событие несёт Status = Hidden; здания и объекты недвижимости не тронуты.
  • AC3 (идемпотентный повтор). Дано: проект уже в TargetStatus. Когда: та же операция повторяется. Тогда: 200 no-op, Version не меняется, повторное событие не публикуется.
  • AC4 (недопустимый переход). Дано: проект в Draft. Когда: попытка Hide/Unpublish (текущий статус не Published и не равен TargetStatus). Тогда: CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION (409), состояние без изменений.
  • AC5 (конкурентная правка). Дано: клиентский Version устарел. Когда: операция. Тогда: CHESSBOARD_CONCURRENCY_CONFLICT (409); проигравший перечитывает проект и повторяет.
  • AC6 (чужой tenant). Дано: проект другого застройщика. Когда: операция. Тогда: CHESSBOARD_PROJECT_NOT_FOUND (404), событие не публикуется.
  • AC7 (невалидный целевой статус). Дано: TargetStatus = Archived (или иное вне {Draft, Hidden}). Когда: операция. Тогда: ValidationError (400).

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

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

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