POST /api/v1/developers/buildings/{buildingId}/status — Смена статуса здания
Назначение
Менеджер застройщика управляет видимостью литера/корпуса в шахматке и ассортименте published-проекта:
вводит корпус в продажу (Publish), возвращает на доработку (Unpublish), временно скрывает очередь не в
продаже (Hide) и возвращает её (Unhide). Конкретный переход задаётся полем Action в теле запроса — один
endpoint вместо четырёх. Дом полей и жизненный цикл — сущность Здание.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | buildings |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/set-building-status.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.buildings.manage |
| Scope (RBAC) | организация / группа проектов / проект |
| Tenant isolation | TenantId = CompanyId из JWT (ADR-0052); чужое здание → 404 |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/buildings/{buildingId}/status |
| Success status | 200 |
| Idempotency header | не применяется — повтор того же Action при уже достигнутом целевом статусе идемпотентен (no-op) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
BuildingId | route | Guid | Да | существующее здание tenant-а | 404 при чужом |
Action | body | BuildingStatusAction (enum-string) | Да | Publish / Unpublish / Hide / Unhide; прочее → ValidationError | — |
Version | body | long | Да | >= 1; optimistic-сверка с BuildingEntity.Version | — |
Enum BuildingStatusAction → матрица переходов
Action — тип-контракт запроса, не доменный enumОднозначно задаёт переход From → To. Целевой BuildingStatus выводится из Action (не принимается
отдельным полем, чтобы исключить неоднозначность → Published между Publish и Unhide).
Action | Допустимый From | To | Guard |
|---|---|---|---|
Publish | Draft | Published | INV-BLD-4 (полнота, ниже) |
Unpublish | Published | Draft | — |
Hide | Published | Hidden | — |
Unhide | Hidden | Published | — (данные уже полны) |
Модель ответа SetDeveloperBuildingStatusResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | BuildingEntity.Id | — | Id здания. |
ProjectId | Guid | Да | ProjectId | — | Проект-родитель. |
Status | BuildingStatus | Да | Status | — | Новый статус (Draft/Published/Hidden). |
Version | long | Да | Version | — | Новая версия. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент перехода. |
Предусловия
- Пользователь авторизован;
tenant_id= его компания-застройщик; есть permissionchessboard.buildings.manageв scope проекта. - Здание существует, принадлежит tenant-у актора и не удалено.
Versionв теле равен текущей доменной версии здания (optimistic guard).- Текущий
Statusвходит в «ДопустимыйFrom» для выбранногоAction(матрица переходов выше); дляAction = Publishдополнительно выполнены guard-ы полноты INV-BLD-4 (проект-родительStatus ∈ {Draft, Published}, заполненыFloorsTotal,DeliveryYear,DeliveryQuarter). - Если здание уже в целевом статусе для
Action— операция идемпотентна (no-op).
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.buildings.manageв scope проекта. Нет permission в scope →FORBIDDEN(403). Если статус уже равен целевому дляAction— идемпотентный no-op: возвращается текущее состояние без нового события. - Проверка входа и предусловий.
Action ∈ {Publish, Unpublish, Hide, Unhide},Version >= 1— иначеValidationError;- здание существует в пределах tenant-а — иначе
CHESSBOARD_BUILDING_NOT_FOUND(404, чужой tenant неотличим от отсутствия); - текущий статус входит в «Допустимый
From» дляAction(матрица выше) — иначеCHESSBOARD_BUILDING_INVALID_STATUS_TRANSITION(409); - для
Action = Publish(INV-BLD-4): проект-родитель имеетStatus ∈ {Draft, Published}и у здания заполненыFloorsTotal,DeliveryYear,DeliveryQuarter; иначеCHESSBOARD_BUILDING_PUBLISH_INCOMPLETE(409, metadatareason = ProjectNotPublishableилиmissingFields = [...]); Versionсовпадает с текущей — иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- Переход статуса. Здание переводится в целевой статус по
Action(матрицаFrom → To); guard-ы переходов и INV-BLD-4 продублированы в проверке. Меняется толькоStatus; проставляютсяUpdatedByUserId, инкрементVersion; прочие поля не трогаются. - Факты и события. Публикуется событие
pin.chessboard.building-upserted.v1— полный снапшот с новымStatus. Отдельного status-топика у здания нет: потребители читаютStatusиз снапшота. - Результат. Возвращаются
Id,ProjectId,Status,Version,UpdatedAt; HTTP200.
Диаграмма
Постусловия
При успешном переходе:
Status= целевой поAction(матрицаFrom → To);UpdatedByUserIdпроставлен,Versionинкрементирован; прочие поля здания не изменены; объекты недвижимости здания не мутируются.- Опубликовано
pin.chessboard.building-upserted.v1— полный снапшот с новымStatus(отдельного status-топика у здания нет). - В публичной шахматке:
Publish/Unhide— корпус и егоPublishedобъекты недвижимости появляются;Unpublish/Hide— выпадают (фильтрStatus = Publishedв query-слоеcatalog, после обработки события).
При идемпотентном повторе (статус уже целевой): состояние не меняется, новое событие не порождается.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | BuildingUpsertedKafkaEvent (новый Status) → pin.chessboard.building-upserted.v1; потребители: catalog (карточка/фильтр «срок сдачи», включение/скрытие корпуса в шахматке), dev-stats (измерение Building) |
| Проекции / поиск | Publish/Unhide — корпус и его Published объекты недвижимости появляются в public-шахматке; Unpublish/Hide — выпадают (фильтр Status = Published в query-слое; объекты недвижимости не мутируются) |
Интеграционная последовательность и согласованность. Переход и публикация снапшота атомарны (outbox).
catalog обновляет карточку/фильтр «срок сдачи» и включение/скрытие корпуса в шахматке асинхронно;
dev-stats обновляет измерение Building. Building.PropertyObjects*Count в событие не входят (дом
building-upserted.md) — счётчики потребители выводят из
property-object-потока. В окне eventual-consistency публичная шахматка может кратко отражать прежнее
состояние корпуса; застройщик видит новый статус в кабинете сразу (read-after-write). Порядок снапшотов по
зданию гарантирован key-ом Id; потребители — upsert-by-id с монотонным Version-guard.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Action вне enum / Version < 1 | Список ошибок полей | no retry |
CHESSBOARD_BUILDING_NOT_FOUND | 404 | Нет здания / чужой tenant | «Здание не найдено» | no retry |
CHESSBOARD_BUILDING_INVALID_STATUS_TRANSITION | 409 | Текущий Status не в «Допустимый From» для Action | «Недопустимый переход статуса» | no retry |
CHESSBOARD_BUILDING_PUBLISH_INCOMPLETE | 409 | INV-BLD-4: Project.Status ∉ {Draft, Published} или не заполнены FloorsTotal/DeliveryYear/DeliveryQuarter; metadata reason/missingFields | «Корпус не готов к публикации» | no retry (заполнить поля) |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет permission | «Недостаточно прав» | no retry |
Acceptance Criteria
- Publish. Given здание
Draftполно по INV-BLD-4, проект-родительDraft/Published, корректныйVersion; WhenAction = Publish; Then200,Status = Published, снапшотbuilding-upsertedопубликован. - Publish неполон. Given
Action = Publish, но не заполненыFloorsTotal/DeliveryYear/DeliveryQuarterили проект вне{Draft, Published}; When запрос; Then409CHESSBOARD_BUILDING_PUBLISH_INCOMPLETEсmissingFields/reason; состояние не изменено. - Unpublish / Hide / Unhide. Given здание в допустимом
FromдляAction; When соответствующийAction; Then200с целевымStatusпо матрице (Published→Draft/Published→Hidden/Hidden→Published). - Недопустимый переход. Given текущий
Statusвне «ДопустимыйFrom» дляAction(напримерHideизDraft); When запрос; Then409CHESSBOARD_BUILDING_INVALID_STATUS_TRANSITION. - Идемпотентный повтор. Given здание уже в целевом для
Actionстатусе; When повторный запрос; Then200с текущим состоянием, без нового события. - Гонка версий. Given
Versionне совпал; When запрос; Then409CHESSBOARD_CONCURRENCY_CONFLICT. - Невалидный вход. Given
Actionвне enum илиVersion < 1; When запрос; Then400ValidationError. - Не найден / нет права. Given здание чужого tenant-а →
404CHESSBOARD_BUILDING_NOT_FOUND; нет permission →403FORBIDDEN.
Совместимость и наблюдаемость
- Контракт стабилен; добавление нового
Action— additive (потребители имеют fallback на unknown enum, правило ../domain/reference-data.md); удаление/переименование значения — breaking.MarkDelivered(ConstructionStatus = Delivered) — отдельная операция, не через этот endpoint. - Logs:
TenantId,ProjectId,BuildingId,Action,From → To; metrics:chessboard_building_status_transition_total{action,from,to}; traces: OTel (traceparent).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/projects/{projectId}/buildings — Создание здания, PUT /api/v1/developers/buildings/{buildingId} — Обновление здания
- События (1): Event Contract: pin.chessboard.building-upserted (BuildingUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/building.md (жизненный цикл, INV-BLD-4,
BuildingStatus); родитель — ../domain/project.md (guardProject.Status). - Создание: create-building.md; правка: update-building.md; реестр: README.md.