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

PUT /api/v1/developers/projects/{projectId} — Обновление проекта

Назначение

Менеджер застройщика редактирует карточку проекта своей компании: атрибуты и пять вкладок карточки за одну полную замену редактируемых полей. Реплика-поля Talent при SyncSource = TalentChessboard через эту операцию менять нельзя (INV-PRJ-9); статус проектом не меняется (см. публикацию/снятие с публикации). Дом полей — сущность Проект.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/developers/projects/{projectId}
Success status200
Idempotency headerне применяется — PUT семантически идемпотентен (full replace + Version)
Correlationсквозной trace через заголовок traceparent

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

Ограничения — как в create-project.md (дом полей project.md); здесь перечислены различия и полный состав.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ProjectIdrouteGuidДасуществующий проект tenant-а404 при чужом
NamebodystringДа1..256; реплика-поле — при SyncSource = TalentChessboard изменение запрещено (INV-PRJ-9)
Descriptionbodystring?Нет<= 8000 (PIN-поле)
TypebodyProjectTypeДаenum; реплика-поле
ClassbodyProjectClassДаenum; реплика-поле
ConstructionStatusbodyConstructionStatusДаenum; реплика-поле
CountryIdbodyshortДасуществующая неархивная страна справочника (INV-PRJ-10); реплика-поле
RegionIdbodylong?Нетсуществующий неархивный регион справочника, согласован со страной (INV-PRJ-10); реплика-поле; для Published-проекта сброс запрещён (INV-PRJ-3)
CityIdbodylong?Нетсуществующий неархивный город справочника, согласован с регионом/страной (INV-PRJ-10); реплика-поле; для Published-проекта сброс запрещён (INV-PRJ-3)
Starsbodyshort?Нет1..5, только для типов отелей; реплика-поле
HotelierIdbodyGuid?Нетсуществующий не архивный отельер (PIN-поле)
LocationbodyProjectLocationDocumentДаtyped-модель (страна/регион/город — колонки-ссылки выше, не документ); address-часть (District/AddressLine/PostalCode) — реплика-часть при синке; Geo, дистанции, TransportNodes — PIN-часть
InfrastructurebodyProjectInfrastructureDocumentДаPIN-поле; ограничения create-док
FinancingbodyProjectFinancingDocumentДаPIN-поле; банки существуют (INV-PRJ-7)
LegalInfobodyProjectLegalInfoDocumentДаPIN-поле; pattern Inn/Ogrn
ConstructionProgressbodyProjectConstructionProgressDocumentДаPIN-поле; ограничения create-док
PresentationVideoUrlbodystring?НетURL ≤ 512 (PIN-поле)
CoverImageFileIdbodyGuid?Нетреплика-поле
PaybackYearsbodydecimal?Нет> 0 (PIN-поле)
VersionbodylongДа>= 1; сверка с ProjectEntity.Version (optimistic)
Sync-guard реплика-полей Talent (INV-PRJ-9)

Пока 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

CurrencyId через update не меняется (INV-PRJ-6 — валюта фиксируется при создании; смена валюты проекта с объектами недвижимости — отдельная admin-операция resolution-класса).

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаProjectEntity.IdId проекта.
VersionlongДаVersion (после инкремента)Новая доменная версия.
UpdatedAtDateTimeOffsetДа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 синка соответственно; эта операция лишь читает их актуальные локальные значения. Если банк/ отельер отсутствует на момент правки — операция отклоняется (см. «Ошибки»), а не пишет «висячую» ссылку.
Окно eventual-consistency

Для published-проекта изменения на витрине catalog и в поиске видны с лагом consumer (цель — секунды/до минуты для поиска). Компенсаций не требуется — снапшот полный, идемпотентен.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.projects.manage в scope, покрывающем projectId.
  2. Проверка входа и предусловий.
    • формат/диапазоны/enum всех полей — иначе ValidationError;
    • проект существует в пределах tenant-а — иначе CHESSBOARD_PROJECT_NOT_FOUND (404);
    • статус проекта ∈ {Draft, Published, Hidden}; ArchivedCHESSBOARD_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).
  3. Применение правок. Набор мутируемых полей зависит от SyncSource (INV-PRJ-9): всегда — описание, отельер, PIN-часть локации (Geo, дистанции, транспортные узлы), вкладки Infrastructure/Financing/LegalInfo/ConstructionProgress (ContractVersion = 1), видео, окупаемость; при SyncSource = Pin дополнительно — реплика-поля и address-часть локации. При смене RegionId/CityId display-имена RegionName/CityName снимаются из реплики справочника (заполнены ⇔ соответствующий id задан). Переходов статуса нет; проставляются UpdatedByUserId, инкремент Version. Правка не должна затирать чужую параллельную (сверка версии) — иначе CHESSBOARD_CONCURRENCY_CONFLICT.
  4. Факты и события. Публикуется событие pin.chessboard.project-upserted.v1 — полный снапшот проекта после правки (состав — project.md «События»).
  5. Результат. Возвращаются Id, Version, UpdatedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияProjectUpsertedKafkaEventpin.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
ValidationError400Нарушение ограничений полейСписок ошибок полейno retry
CHESSBOARD_PROJECT_NOT_FOUND404Нет проекта / чужой tenant / вне scope«Проект не найден»no retry
CHESSBOARD_PROJECT_ARCHIVED409Status = Archived«Проект в архиве»no retry
CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED409Попытка сменить реплика-поле Talent-реплики (INV-PRJ-9)«Поле управляется синхронизацией Talent»no retry
CHESSBOARD_PROJECT_HOTELIER_REQUIRED409Сброс отельера у published-отеля (INV-PRJ-4)«Для отеля обязателен отельер»no retry
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE409Сброс резолвленных RegionId/CityId у Published-проекта (INV-PRJ-3)«Проект не готов к публикации»no retry
CHESSBOARD_GEO_REF_UNKNOWN422Справочная ссылка (CountryId/RegionId/CityId) не найдена (INV-PRJ-10)«Запись справочника не найдена»no retry
CHESSBOARD_GEO_REF_ARCHIVED422Указана архивная запись справочника (INV-PRJ-10)«Запись справочника архивирована»no retry
CHESSBOARD_CITY_REGION_MISMATCH422Город/регион/страна рассогласованы (INV-PRJ-10)«Гео-цепочка не согласована»no retry
CHESSBOARD_HOTELIER_NOT_FOUND400HotelierId не найден/архив«Отельер не найден»no retry
CHESSBOARD_PROJECT_BANK_NOT_FOUND400Банк из Financing не существует (INV-PRJ-7)«Банк не найден»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал / параллельная правка«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет permission«Недостаточно прав»no retry

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

  • Новые optional поля — additive; вкладки — правила ContractVersion project.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), не эта операция.

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

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

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