PUT /api/v1/developers/projects/{projectId} — Обновление проекта
Назначение
Менеджер застройщика редактирует карточку проекта своей компании: атрибуты и пять вкладок карточки за одну
полную замену редактируемых полей. Реплика-поля Talent при SyncSource = TalentChessboard через эту
операцию менять нельзя (INV-PRJ-9); статус проектом не меняется (см.
публикацию/снятие с публикации). Дом полей — сущность
Проект.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | projects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/update-project.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.projects.manage |
| Scope (RBAC) | организация / группа проектов / проект (projectId в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; чужой проект → 404 |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/developers/projects/{projectId} |
| Success status | 200 |
| Idempotency header | не применяется — PUT семантически идемпотентен (full replace + Version) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Ограничения — как в create-project.md (дом полей project.md); здесь перечислены различия и полный состав.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ProjectId | route | Guid | Да | существующий проект tenant-а | 404 при чужом |
Name | body | string | Да | 1..256; реплика-поле — при SyncSource = TalentChessboard изменение запрещено (INV-PRJ-9) | — |
Description | body | string? | Нет | <= 8000 (PIN-поле) | — |
Type | body | ProjectType | Да | enum; реплика-поле | — |
Class | body | ProjectClass | Да | enum; реплика-поле | — |
ConstructionStatus | body | ConstructionStatus | Да | enum; реплика-поле | — |
CountryId | body | short | Да | существующая неархивная страна справочника (INV-PRJ-10); реплика-поле | — |
RegionId | body | long? | Нет | существующий неархивный регион справочника, согласован со страной (INV-PRJ-10); реплика-поле; для Published-проекта сброс запрещён (INV-PRJ-3) | — |
CityId | body | long? | Нет | существующий неархивный город справочника, согласован с регионом/страной (INV-PRJ-10); реплика-поле; для Published-проекта сброс запрещён (INV-PRJ-3) | — |
Stars | body | short? | Нет | 1..5, только для типов отелей; реплика-поле | — |
HotelierId | body | Guid? | Нет | существующий не архивный отельер (PIN-поле) | — |
Location | body | ProjectLocationDocument | Да | typed-модель (страна/регион/город — колонки-ссылки выше, не документ); address-часть (District/AddressLine/PostalCode) — реплика-часть при синке; Geo, дистанции, TransportNodes — PIN-часть | — |
Infrastructure | body | ProjectInfrastructureDocument | Да | PIN-поле; ограничения create-док | — |
Financing | body | ProjectFinancingDocument | Да | PIN-поле; банки существуют (INV-PRJ-7) | — |
LegalInfo | body | ProjectLegalInfoDocument | Да | PIN-поле; pattern Inn/Ogrn | — |
ConstructionProgress | body | ProjectConstructionProgressDocument | Да | PIN-поле; ограничения create-док | — |
PresentationVideoUrl | body | string? | Нет | URL ≤ 512 (PIN-поле) | — |
CoverImageFileId | body | Guid? | Нет | реплика-поле | — |
PaybackYears | body | decimal? | Нет | > 0 (PIN-поле) | — |
Version | body | long | Да | >= 1; сверка с ProjectEntity.Version (optimistic) | — |
Пока SyncSource = TalentChessboard, поля Name, Type, Class, ConstructionStatus, гео-поля
CountryId/RegionId/CityId, Stars, CoverImageFileId и address-часть Location
(District/AddressLine/PostalCode) управляются синхронизацией Talent — правке через этот метод не
подлежат, их значения в запросе должны совпадать с текущими, иначе CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED
(409). Редактируемо всегда — описание, отельер, PIN-часть локации (Geo, дистанции, транспортные узлы),
вкладки Infrastructure/Financing/LegalInfo/ConstructionProgress, видео, окупаемость.
CurrencyId через update не меняется (INV-PRJ-6 — валюта фиксируется при создании; смена валюты проекта с
объектами недвижимости — отдельная admin-операция resolution-класса).
Модель ответа UpdateDeveloperProjectResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | ProjectEntity.Id | — | Id проекта. |
Version | long | Да | Version (после инкремента) | — | Новая доменная версия. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент сохранения. |
Полная карточка перечитывается через get-project.md.
Предусловия и постусловия
Предусловия:
- Актор авторизован и имеет
chessboard.projects.manageв scope, покрывающемprojectId. - Проект существует в tenant актора и находится в статусе ∈ {
Draft,Published,Hidden} (Archived— редактирование запрещено). - Клиент располагает актуальным
Version. - Все банки из
Financingсуществуют (INV-PRJ-7); при заданномHotelierId— отельер существует и не архивен; реплика-поля Talent редактируемы только приSyncSource = Pin.
Постусловия при успехе:
- Обновлён набор полей, разрешённый
SyncSource(приTalentChessboard— PIN-поля и PIN-часть локации; приPin— плюс реплика-поля и address-часть локации);Version+1,UpdatedByUserId/UpdatedAtпроставлены;StatusиCurrencyId(INV-PRJ-6) не меняются. - Инвариант «для published-отеля обязателен отельер» (INV-PRJ-4) не нарушен.
- В том же согласованном изменении поставлено на публикацию
pin.chessboard.project-upserted.v1с полным снапшотом проекта после правки.
Постусловие при отказе: проект не изменён, событие не публикуется, Version сохранён.
Интеграционная последовательность
- Синхронно возвращается
200+Version/UpdatedAt; синхронных вызовов соседних сервисов операция не делает (отельер/банки читаются из локальных реплик chessboard). - Асинхронно
pin.chessboard.project-upserted.v1(партиционный ключProjectId, порядок per-project гарантирован) потребляют:catalog(обновление витрины published-проекта),dev-stats,identity,news; поиск обновляетProjectSearchDocument(name/address, ADR-0059). Потребители применяют снапшоты монотонно поVersion; переупорядочивание/дубли безопасны. - Ожидания от других контекстов: справочники банков/отельеров и реплика-поля Talent ведут админ-операции и consumer синка соответственно; эта операция лишь читает их актуальные локальные значения. Если банк/ отельер отсутствует на момент правки — операция отклоняется (см. «Ошибки»), а не пишет «висячую» ссылку.
Для published-проекта изменения на витрине catalog и в поиске видны с лагом consumer (цель — секунды/до
минуты для поиска). Компенсаций не требуется — снапшот полный, идемпотентен.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.projects.manageв scope, покрывающемprojectId. - Проверка входа и предусловий.
- формат/диапазоны/enum всех полей — иначе
ValidationError; - проект существует в пределах tenant-а — иначе
CHESSBOARD_PROJECT_NOT_FOUND(404); - статус проекта ∈ {
Draft,Published,Hidden};Archived→CHESSBOARD_PROJECT_ARCHIVED(409); - sync-guard (INV-PRJ-9): при
SyncSource = TalentChessboardлюбое отличие входных реплика-полей (Name,Type,Class,ConstructionStatus, гео-поляCountryId/RegionId/CityId,Stars,CoverImageFileId, address-частьLocation) от текущих →CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED(409); - справочные ссылки (INV-PRJ-10, при изменении гео-полей, по реплике
../domain/handbook-replica.md):
CountryId/RegionId/CityIdсуществуют и не архивны, гео-цепочка согласована — иначеCHESSBOARD_GEO_REF_UNKNOWN/CHESSBOARD_GEO_REF_ARCHIVED/CHESSBOARD_CITY_REGION_MISMATCH(422); сброс резолвленныхRegionId/CityIdуPublished-проекта запрещён (INV-PRJ-3) →CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE(409); - если
HotelierIdзадан и изменился — отельер существует и не архивен, иначеCHESSBOARD_HOTELIER_NOT_FOUND; сброс отельера у published-проекта типа отель запрещён (INV-PRJ-4) →CHESSBOARD_PROJECT_HOTELIER_REQUIRED; - все банки из
Financingсуществуют — иначеCHESSBOARD_PROJECT_BANK_NOT_FOUND(INV-PRJ-7); Versionсовпадает с текущей версией проекта — иначеCHESSBOARD_CONCURRENCY_CONFLICT(409).
- формат/диапазоны/enum всех полей — иначе
- Применение правок. Набор мутируемых полей зависит от
SyncSource(INV-PRJ-9): всегда — описание, отельер, PIN-часть локации (Geo, дистанции, транспортные узлы), вкладкиInfrastructure/Financing/LegalInfo/ConstructionProgress(ContractVersion = 1), видео, окупаемость; приSyncSource = Pinдополнительно — реплика-поля и address-часть локации. При сменеRegionId/CityIddisplay-именаRegionName/CityNameснимаются из реплики справочника (заполнены ⇔ соответствующий id задан). Переходов статуса нет; проставляютсяUpdatedByUserId, инкрементVersion. Правка не должна затирать чужую параллельную (сверка версии) — иначеCHESSBOARD_CONCURRENCY_CONFLICT. - Факты и события. Публикуется событие
pin.chessboard.project-upserted.v1— полный снапшот проекта после правки (состав — project.md «События»). - Результат. Возвращаются
Id,Version,UpdatedAt; HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | ProjectUpsertedKafkaEvent → pin.chessboard.project-upserted.v1, key = ProjectId; потребители: catalog (обновление витрины published-проекта), dev-stats, identity, news |
| Проекции / поиск | Elasticsearch-проекция ProjectSearchDocument (name/address) обновляется по событию pin.chessboard.project-upserted.v1 (../../search/projects.md, ADR-0059) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение ограничений полей | Список ошибок полей | no retry |
CHESSBOARD_PROJECT_NOT_FOUND | 404 | Нет проекта / чужой tenant / вне scope | «Проект не найден» | no retry |
CHESSBOARD_PROJECT_ARCHIVED | 409 | Status = Archived | «Проект в архиве» | no retry |
CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED | 409 | Попытка сменить реплика-поле Talent-реплики (INV-PRJ-9) | «Поле управляется синхронизацией Talent» | no retry |
CHESSBOARD_PROJECT_HOTELIER_REQUIRED | 409 | Сброс отельера у published-отеля (INV-PRJ-4) | «Для отеля обязателен отельер» | no retry |
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE | 409 | Сброс резолвленных RegionId/CityId у Published-проекта (INV-PRJ-3) | «Проект не готов к публикации» | no retry |
CHESSBOARD_GEO_REF_UNKNOWN | 422 | Справочная ссылка (CountryId/RegionId/CityId) не найдена (INV-PRJ-10) | «Запись справочника не найдена» | no retry |
CHESSBOARD_GEO_REF_ARCHIVED | 422 | Указана архивная запись справочника (INV-PRJ-10) | «Запись справочника архивирована» | no retry |
CHESSBOARD_CITY_REGION_MISMATCH | 422 | Город/регион/страна рассогласованы (INV-PRJ-10) | «Гео-цепочка не согласована» | no retry |
CHESSBOARD_HOTELIER_NOT_FOUND | 400 | HotelierId не найден/архив | «Отельер не найден» | no retry |
CHESSBOARD_PROJECT_BANK_NOT_FOUND | 400 | Банк из Financing не существует (INV-PRJ-7) | «Банк не найден» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал / параллельная правка | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет permission | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional поля — additive; вкладки — правила
ContractVersionproject.md. - Logs:
TenantId,ProjectId, изменённые поля (имена, без значений); metrics:chessboard_developers_project_update_total{result}; traces: OTel (traceparent).
Acceptance Criteria
- AC1 (правка вкладок карточки). Дано: проект
SyncSource = Pin, статусPublished, актуальныйVersion. Когда: меняются описание, инфраструктура, финансирование, локация. Тогда: поля обновлены,Version+1, публикуетсяpin.chessboard.project-upserted.v1; витринаcatalogи поиск обновляются после обработки события. - AC2 (sync-guard реплики). Дано:
SyncSource = TalentChessboard. Когда: входное реплика-поле (Name/Type/Class/гео-поляCountryId/RegionId/CityId/… или address-частьLocation) отличается от текущего. Тогда:CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED(409); PIN-поля и PIN-часть локации при этом применяются. - AC3 (архивный проект). Дано:
Status = Archived. Когда: правка. Тогда:CHESSBOARD_PROJECT_ARCHIVED(409), состояние без изменений. - AC4 (обязательный отельер у published-отеля). Дано: published-проект типа отель с отельером. Когда:
HotelierIdсбрасывается вnull. Тогда:CHESSBOARD_PROJECT_HOTELIER_REQUIRED(409, INV-PRJ-4). - AC5 (несуществующий банк/отельер). Дано:
Financingссылается на несуществующий банк (или задан архивный/отсутствующийHotelierId). Когда: правка. Тогда:CHESSBOARD_PROJECT_BANK_NOT_FOUND/CHESSBOARD_HOTELIER_NOT_FOUND(400), «висячая» ссылка не сохраняется. - AC6 (конкурентная правка). Дано: устаревший
Version. Когда: сохранение. Тогда:CHESSBOARD_CONCURRENCY_CONFLICT(409); проигравший перечитывает и повторяет. - AC7 (чужой tenant/вне scope). Дано: проект другого застройщика или вне scope актора. Когда: правка.
Тогда:
CHESSBOARD_PROJECT_NOT_FOUND(404), событие не публикуется. - AC8 (битая справочная ссылка гео). Дано: проект
SyncSource = Pin; входнойCityIdнеизвестен/архивен либо не согласован сRegionId/CountryId. Когда: правка. Тогда:CHESSBOARD_GEO_REF_UNKNOWN/CHESSBOARD_GEO_REF_ARCHIVED/CHESSBOARD_CITY_REGION_MISMATCH(422, INV-PRJ-10); проект не изменён, событие не публикуется. - AC9 (переименование региона/города). Дано: сменился
CityIdпроекта. Когда: сохранение. Тогда:CityNameперечитан из реплики справочника; последующий refresh display-имён при переименованиях в справочнике — consumer ../consumers/handbook-sync.md (HR-6), не эта операция.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId} — Карточка проекта застройщика, POST /api/v1/developers/projects — Создание проекта
- Консюмеры (1): Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/project.md (INV-PRJ-3/4/6/7/9/10); реплика справочников handbook: ../domain/handbook-replica.md.
- Событие: ../events/project-upserted.md (снапшот, партиционный ключ, версионирование).
- Чтение: get-project.md; статусы: publish-project.md, unpublish-project.md; реестр: README.md.