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

POST /api/v1/developers/projects/{projectId}/publish — Публикация проекта

Назначение

Менеджер застройщика публикует заполненный проект: переход Draft → Published (или Hidden → Published, операция Unhide той же семантики). После публикации проект попадает в публичную ленту, поиск и фильтры каталога. Guard-ы полноты — INV-PRJ-3 (обязательные поля) и INV-PRJ-4 (отельер для отелей), плюс требование, что застройщик сам опубликован (Developer.Status = Published). Дом полей и жизненный цикл — сущность Проект.

Metadata

ПолеЗначение
Сервис/контекстchessboard
API areadevelopers
Feature groupprojects
Статусdraft
Документdocs/06-services/chessboard/api/developers/publish-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}/publish
Success status200
Idempotency headerне применяется — повторный publish уже опубликованного идемпотентно возвращает текущее состояние (no-op)
Correlationсквозной trace через заголовок traceparent

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

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

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

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

Предусловия

  • Пользователь авторизован; tenant_id = его компания-застройщик; есть permission chessboard.projects.manage в scope проекта.
  • Проект существует, принадлежит tenant-у актора и не удалён.
  • Version в теле равен текущей доменной версии проекта (optimistic guard).
  • Статус проекта ∈ {Draft, Hidden} (из Archived публиковать нельзя).
  • Застройщик-родитель опубликован (Developer.Status = Published, INV-DEV-6).
  • Полнота INV-PRJ-3: заполнены Name, Description, CoverImageFileId, Location.Geo; резолвлены справочные ссылки RegionId и CityId (не null).
  • INV-PRJ-4: для Type ∈ {ApartHotel, Hotel} задан HotelierId.
Идемпотентность

Если проект уже Published — операция идемпотентна: 200 с текущим состоянием, без изменений и без нового события.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.projects.manage. Нет permission в scope → FORBIDDEN (403). Если проект уже Published — идемпотентный no-op: возвращается текущее состояние без изменений и нового события.
  2. Проверка входа и предусловий.
    • формат: ProjectId — Guid, Version >= 1;
    • проект существует в пределах tenant-а — иначе CHESSBOARD_PROJECT_NOT_FOUND (404);
    • статус ∈ {Draft, Hidden} (допустимые переходы жизненного цикла); ArchivedCHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION (409);
    • застройщик-родитель опубликован (Developer.Status = Published); иначе CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE (metadata reason = DeveloperNotPublished);
    • полнота INV-PRJ-3: заполнены Name, Description, CoverImageFileId, Location.Geo; резолвлены справочные ссылки RegionId и CityId (не null); иначе CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE (metadata missingFields = [...]);
    • INV-PRJ-4: для Type ∈ {ApartHotel, Hotel} задан HotelierId; иначе CHESSBOARD_PROJECT_HOTELIER_REQUIRED;
    • Version совпадает с текущей — иначе CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Переход статуса. Проект переводится Draft → Published либо Hidden → Published (Unhide); guard-ы полноты (INV-PRJ-3/4) продублированы в проверке. Меняется только Status; проставляются UpdatedByUserId, инкремент Version.
  4. Факты и события. Публикуется событие pin.chessboard.project-upserted.v1 — полный снапшот с новым Status = Published. Отдельного status-топика у проекта нет: потребители читают Status из снапшота.
  5. Результат. Возвращаются Id, Status = Published, Version, UpdatedAt; HTTP 200.

Диаграмма

Постусловия

При успешном переходе Draft → Published (или Hidden → Published):

  • Status = Published, UpdatedByUserId проставлен, Version инкрементирован; прочие поля не изменены.
  • Опубликовано pin.chessboard.project-upserted.v1 — полный снапшот с Status = Published.
  • Проект индексируется в Elasticsearch-проекцию ProjectSearchDocument и начинает попадать в публичную ленту, поиск и фильтры каталога.
  • catalog включает проект в ленту, dev-stats активирует измерение Project, identity разрешает scope-назначения на проект, news открывает привязку новостей к проекту.

При идемпотентном повторе (проект уже Published): состояние не меняется, новое событие не порождается.

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

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

Интеграционная последовательность и согласованность. Переход и публикация снапшота атомарны (outbox). Downstream-потребители реагируют асинхронно: catalog (лента/карточка), dev-stats (измерение), identity (scope-назначения), news (привязка), плюс индексация ES-проекции. В окне eventual-consistency инвестор может не сразу видеть проект в публичной ленте/поиске; менеджер видит Published в кабинете сразу (read-after-write). Порядок снапшотов по проекту гарантирован key-ом Id; потребители — upsert-by-id с монотонным Version-guard. При потере published-снапшота проект остаётся Published в шахматке, но downstream можно досогласовать повторной публикацией снапшота (recalc/переиндексация).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
CHESSBOARD_PROJECT_NOT_FOUND404Нет проекта / чужой tenant«Проект не найден»no retry
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE409INV-PRJ-3: не заполнены обязательные поля или Developer.Status != Published; metadata: missingFields / reason«Проект не готов к публикации»no retry (заполнить поля)
CHESSBOARD_PROJECT_HOTELIER_REQUIRED409INV-PRJ-4: отель без отельера«Для отеля обязателен отельер»no retry
CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION409Status = Archived«Недопустимый переход статуса»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет permission«Недостаточно прав»no retry

Acceptance Criteria

  • Happy path. Given проект Draft, полный по INV-PRJ-3, застройщик Published, корректный Version; When POST .../publish; Then 200, Status = Published, снапшот project-upserted опубликован, проект индексируется в ленту/поиск.
  • Из Hidden. Given проект Hidden; When POST .../publish; Then 200, Status = Published (семантика unhide).
  • Неполон. Given не заполнено обязательное поле INV-PRJ-3; When запрос; Then 409 CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE с missingFields; состояние не изменено.
  • Нерезолвленные гео-ссылки. Given проект из синка Talent, RegionId/CityId не резолвлены name-match-ем (../consumers/talent-chessboard-sync.md); When запрос; Then 409 CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE (missingFields = [RegionId, CityId]) до ручного разрешения ссылок.
  • Застройщик не опубликован. Given Developer.Status != Published; When запрос; Then 409 CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE (reason = DeveloperNotPublished).
  • Отель без отельера. Given Type ∈ {ApartHotel, Hotel} и HotelierId = null; When запрос; Then 409 CHESSBOARD_PROJECT_HOTELIER_REQUIRED (INV-PRJ-4).
  • Из архива. Given проект Archived; When запрос; Then 409 CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION.
  • Идемпотентный повтор. Given проект уже Published; When повторный запрос; Then 200 с текущим состоянием, без нового события.
  • Гонка версий. Given Version не совпал; When запрос; Then 409 CHESSBOARD_CONCURRENCY_CONFLICT.
  • Не найден / нет права. Given проект чужого tenant-а → 404 CHESSBOARD_PROJECT_NOT_FOUND; нет permission → 403 FORBIDDEN.

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

  • Контракт стабилен; расширение guard-ов публикации — только с ростом кода missingFields (additive).
  • Logs: TenantId, ProjectId, From → Published; metrics: chessboard_project_status_transition_total{from,to}; traces: OTel.

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

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

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