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 area | developers |
| Feature group | projects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/publish-project.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.projects.manage |
| Scope (RBAC) | организация / группа проектов / проект |
| Tenant isolation | TenantId = CompanyId из JWT; чужой проект → 404 |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/projects/{projectId}/publish |
| Success status | 200 |
| Idempotency header | не применяется — повторный publish уже опубликованного идемпотентно возвращает текущее состояние (no-op) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ProjectId | route | Guid | Да | существующий проект tenant-а | 404 при чужом |
Version | body | long | Да | >= 1; сверка с ProjectEntity.Version | — |
Модель ответа PublishDeveloperProjectResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | ProjectEntity.Id | — | Id проекта. |
Status | ProjectStatus | Да | Status | — | Published. |
Version | long | Да | Version | — | Новая версия. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент перехода. |
Предусловия
- Пользователь авторизован;
tenant_id= его компания-застройщик; есть permissionchessboard.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 с текущим состоянием, без изменений и без
нового события.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.projects.manage. Нет permission в scope →FORBIDDEN(403). Если проект ужеPublished— идемпотентный no-op: возвращается текущее состояние без изменений и нового события. - Проверка входа и предусловий.
- формат:
ProjectId— Guid,Version >= 1; - проект существует в пределах tenant-а — иначе
CHESSBOARD_PROJECT_NOT_FOUND(404); - статус ∈ {
Draft,Hidden} (допустимые переходы жизненного цикла);Archived→CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION(409); - застройщик-родитель опубликован (
Developer.Status = Published); иначеCHESSBOARD_PROJECT_PUBLISH_INCOMPLETE(metadatareason = DeveloperNotPublished); - полнота INV-PRJ-3: заполнены
Name,Description,CoverImageFileId,Location.Geo; резолвлены справочные ссылкиRegionIdиCityId(неnull); иначеCHESSBOARD_PROJECT_PUBLISH_INCOMPLETE(metadatamissingFields = [...]); - INV-PRJ-4: для
Type ∈ {ApartHotel, Hotel}заданHotelierId; иначеCHESSBOARD_PROJECT_HOTELIER_REQUIRED; Versionсовпадает с текущей — иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- формат:
- Переход статуса. Проект переводится
Draft → PublishedлибоHidden → Published(Unhide); guard-ы полноты (INV-PRJ-3/4) продублированы в проверке. Меняется толькоStatus; проставляютсяUpdatedByUserId, инкрементVersion. - Факты и события. Публикуется событие
pin.chessboard.project-upserted.v1— полный снапшот с новымStatus = Published. Отдельного status-топика у проекта нет: потребители читаютStatusиз снапшота. - Результат. Возвращаются
Id,Status = Published,Version,UpdatedAt; HTTP200.
Диаграмма
Постусловия
При успешном переходе 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_FOUND | 404 | Нет проекта / чужой tenant | «Проект не найден» | no retry |
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE | 409 | INV-PRJ-3: не заполнены обязательные поля или Developer.Status != Published; metadata: missingFields / reason | «Проект не готов к публикации» | no retry (заполнить поля) |
CHESSBOARD_PROJECT_HOTELIER_REQUIRED | 409 | INV-PRJ-4: отель без отельера | «Для отеля обязателен отельер» | no retry |
CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION | 409 | Status = Archived | «Недопустимый переход статуса» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет permission | «Недостаточно прав» | no retry |
Acceptance Criteria
- Happy path. Given проект
Draft, полный по INV-PRJ-3, застройщикPublished, корректныйVersion; WhenPOST .../publish; Then200,Status = Published, снапшотproject-upsertedопубликован, проект индексируется в ленту/поиск. - Из Hidden. Given проект
Hidden; WhenPOST .../publish; Then200,Status = Published(семантика unhide). - Неполон. Given не заполнено обязательное поле INV-PRJ-3; When запрос; Then
409CHESSBOARD_PROJECT_PUBLISH_INCOMPLETEсmissingFields; состояние не изменено. - Нерезолвленные гео-ссылки. Given проект из синка Talent,
RegionId/CityIdне резолвлены name-match-ем (../consumers/talent-chessboard-sync.md); When запрос; Then409CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE(missingFields = [RegionId, CityId]) до ручного разрешения ссылок. - Застройщик не опубликован. Given
Developer.Status != Published; When запрос; Then409CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE(reason = DeveloperNotPublished). - Отель без отельера. Given
Type ∈ {ApartHotel, Hotel}иHotelierId = null; When запрос; Then409CHESSBOARD_PROJECT_HOTELIER_REQUIRED(INV-PRJ-4). - Из архива. Given проект
Archived; When запрос; Then409CHESSBOARD_PROJECT_INVALID_STATUS_TRANSITION. - Идемпотентный повтор. Given проект уже
Published; When повторный запрос; Then200с текущим состоянием, без нового события. - Гонка версий. Given
Versionне совпал; When запрос; Then409CHESSBOARD_CONCURRENCY_CONFLICT. - Не найден / нет права. Given проект чужого tenant-а →
404CHESSBOARD_PROJECT_NOT_FOUND; нет permission →403FORBIDDEN.
Совместимость и наблюдаемость
- Контракт стабилен; расширение guard-ов публикации — только с ростом кода
missingFields(additive). - Logs:
TenantId,ProjectId,From → Published; metrics:chessboard_project_status_transition_total{from,to}; traces: OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/projects — Создание проекта, POST /api/v1/developers/projects/{projectId}/unpublish — Снятие проекта с публикации, PUT /api/v1/developers/projects/{projectId} — Обновление проекта
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/project.md (жизненный цикл, INV-PRJ-3/4); родитель: ../domain/developer.md (INV-DEV-6).
- Обратная операция: unpublish-project.md; реестр: README.md.