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

PUT /api/v1/developers/property-objects/{propertyObjectId} — Обновление объекта недвижимости (паспортные поля)

Назначение

Менеджер застройщика правит паспортные поля объекта недвижимости: номер, этаж, секция, комнатность, площади, высота потолков, планировка, виды из окон, вместимость, описание. Статус и цена меняются не здесь: статусы — только событиями CRM (INV-CHS-001), цена — отдельной операцией изменения цены (INV-CHS-008). Реплика-поля Talent при SyncSource = TalentChessboard через API не меняются (sync-guard, аналог INV-BLD-7 / INV-LAY-6). Дом полей — сущность Объект недвижимости.

Metadata

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

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

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

HTTP-контракт

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

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

Признак «реплика-поле» = поле, владелец которого Talent Chessboard при SyncSource = TalentChessboard (перечень — поле SyncSource в property-object.md); правка блокируется sync-guard-ом (шаг 2). Признак «PIN-поле» = локальное поле PIN, редактируемо всегда.

ПолеИсточникТипОбязательностьВалидация / ОграниченияПризнак
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
PropertyObjectIdrouteGuidДасуществующий объект недвижимости tenant-а404 при чужом
NumberbodystringДа1..32; уникален в здании (INV-CHS-006)реплика-поле
FloorbodyshortДа-5..200; <= Building.FloorsTotal (INV-BLD-3)реплика-поле
Sectionbodyshort?Нет1..50; <= Building.SectionsCountреплика-поле
RoomsbodyshortДа0..20; 0 = студияреплика-поле
TotalAreabodydecimalДа> 0, numeric(8,2); смена → пересчёт PricePerMeterреплика-поле
LivingAreabodydecimal?Нет> 0 и <= TotalArea, numeric(8,2)реплика-поле
CeilingHeightMetersbodydecimal?Нет2..10, numeric(4,2)PIN-поле
LayoutIdbodyGuid?Нетпланировка того же проекта, не архивная (INV-CHS-007)реплика-поле (привязка)
WindowViewsbodyPropertyObjectWindowViewsInputДа (может быть пустой)typed-модель нижереплика-поле
Capacitybodyshort?Нет1..20 (апарт-отели)PIN-поле
Descriptionbodystring?Нет0..4000PIN-поле
VersionbodylongДа>= 1; optimistic-сверка с PropertyObjectEntity.Version

Модель ввода PropertyObjectWindowViewsInput

Зеркало PropertyObjectWindowViewsDocument (property-object.md); ContractVersion проставляет сервер (= 1).

ПолеТипОбязательностьОграничения
ViewsIReadOnlyList<WindowViewKind> (enum-строки)Да (может быть пустым)значения WindowViewKind (reference-data.md); ≤ 4, без дублей
PanoramaUrlstring?Нетvarchar(512), URL 3D-тура/панорамы

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPropertyObjectEntity.IdId объекта недвижимости.
VersionlongДаPropertyObjectEntity.Version (после инкремента)Новая доменная версия.
PricePerMeterdecimalДаPropertyObjectEntity.PricePerMeterЦена за м² (пересчитана при смене TotalArea).
UpdatedAtDateTimeOffsetДаPropertyObjectEntity.UpdatedAtМомент сохранения.

Предусловия и постусловия

Предусловия:

  • Актор авторизован и имеет chessboard.property-objects.manage в scope проекта объекта недвижимости.
  • Объект недвижимости существует в tenant актора и не удалён (паспортная правка допустима при любом Status, включая Reserved/Booked/Sold — метод не трогает Status/таймеры/Price).
  • Клиент располагает актуальным Version.
  • Реплика-поля Talent редактируемы только при SyncSource = Pin; целевая планировка (LayoutId) — того же проекта и не архивна (INV-CHS-007).

Постусловия при успехе:

  • Обновлён набор паспортных полей, разрешённый SyncSource (всегда — PIN-поля CeilingHeightMeters/ Capacity/Description; при Pin — плюс реплика-поля номер/этаж/секция/комнатность/площади/виды/ привязка планировки); при смене TotalArea пересчитан PricePerMeter (сам Price не изменён); Version +1, UpdatedByUserId/UpdatedAt проставлены.
  • Инварианты уникальности номера в здании (INV-CHS-006), габаритов этажа/секции (INV-BLD-3), «планировка того же проекта» (INV-CHS-007) не нарушены. Status и Price не изменены (INV-CHS-001/008 — их каналы другие).
  • В том же согласованном изменении поставлено на публикацию pin.chessboard.property-object-upserted.v1 с полным паспортным снапшотом (новое PricePerMeter — в этом снапшоте); при смене LayoutId дополнительно запрошен фоновый пересчёт агрегатов планировок.

Постусловие при отказе: объект недвижимости не изменён, событие не публикуется, Version сохранён.

Интеграционная последовательность

  • Синхронно возвращается 200 + Version/PricePerMeter/UpdatedAt; синхронных вызовов соседей нет.
  • Асинхронно pin.chessboard.property-object-upserted.v1 (партиционный ключ PropertyObjectId, порядок событий по объекту гарантирован, 6 партиций) потребляют: catalog (проекция карточки/списка), dev-stats (измерение PropertyObject), rewards, invest-analytics. Потребители применяют снапшоты монотонно по Version; тот же счётчик Version синхронизирует порядок с событиями статуса/цены того же объекта.
  • Разделение каналов: статус меняют только события CRM (property-object-status-changed), цену — только операция цены (property-object-price-changed); паспортный upsert их не публикует и не перетирает — потребитель, увидев upsert с меньшим Version, чем уже применённое статус/цена-событие, не откатывает состояние.
  • Событийная цепочка пересчёта: при смене LayoutId запрашивается project-aggregates-recalc (debounce 30 с) → job пересчитывает Layout.PropertyObjectsCount старой и новой планировки → следствие приходит потребителям следующим снапшотом планировки (окно debounce).
  • Окно eventual-consistency: новое PricePerMeter/паспортные поля на витрине видны с лагом consumer; счётчики планировок — с лагом debounce. Компенсаций не требуется — снапшот полный, идемпотентен.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.property-objects.manage.
  2. Проверка входа и предусловий.
    • формат/диапазоны полей — иначе ValidationError;
    • объект недвижимости существует в пределах tenant-а — иначе CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND (404);
    • объект не удалён; статус-guard не применяется — паспортная правка допустима при любом Status (в отличие от цены, INV-CHS-008); метод не трогает Status/таймеры/Price;
    • sync-guard (аналог INV-BLD-7 / INV-LAY-6): при SyncSource = TalentChessboard любое отличие входных реплика-полей (Number, Floor, Section, Rooms, TotalArea, LivingArea, WindowViews, LayoutId) от текущих → CHESSBOARD_PROPERTY_OBJECT_FIELD_SYNC_MANAGED (409); PIN-поля (CeilingHeightMeters, Capacity, Description) при этом редактируемы;
    • при смене номера у Pin-объекта — номер уникален в здании (INV-CHS-006), иначе CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN (409);
    • при смене LayoutId — планировка существует, не архивна и того же проекта (INV-CHS-007), иначе CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH (409);
    • при смене Floor/Section у Pin-объекта — они в габаритах здания (INV-BLD-3), иначе ValidationError;
    • Version совпадает с текущей — иначе CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Применение правок. Набор мутируемых полей зависит от SyncSource: всегда — PIN-поля (высота потолков, вместимость, описание); при SyncSource = Pin дополнительно — реплика-поля (номер, этаж, секция, комнатность, площади, виды из окон ContractVersion = 1, привязка планировки). При изменении TotalArea пересчитывается PricePerMeter = round(Price / TotalArea, 2) (сам Price не меняется — price-события нет). Проставляются UpdatedByUserId, инкремент Version.
  4. Факты и события. Публикуется событие pin.chessboard.property-object-upserted.v1 — полный паспортный снапшот после правки; новое PricePerMeter доставляется потребителям в этом снапшоте. Событие цены и события статуса не публикуются. При смене LayoutId запрашивается фоновый пересчёт агрегатов (project-aggregates-recalc, debounce 30 с) — Layout.PropertyObjectsCount старой и новой планировки.
  5. Результат. Возвращаются Id, Version, PricePerMeter, UpdatedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияPropertyObjectUpsertedKafkaEventpin.chessboard.property-object-upserted.v1; потребители: catalog (проекция карточки/списка), dev-stats (измерение PropertyObject), rewards, invest-analytics
Проекции / пересчётProjectAggregatesRecalcRequestedEvent (только при смене LayoutId) → job project-aggregates-recalc (пересчёт Layout.PropertyObjectsCount)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нарушение format/min/max/enum, Floor/Section вне габаритов здания (INV-BLD-3)Список ошибок полейno retry
CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND404Нет объекта недвижимости / чужой tenant«Объект недвижимости не найден»no retry
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN409Дубль номера в здании (INV-CHS-006)«Номер объекта занят»no retry
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH409Планировка чужого проекта/архивная (INV-CHS-007)«Планировка недоступна для этого объекта недвижимости»no retry
CHESSBOARD_PROPERTY_OBJECT_FIELD_SYNC_MANAGED409Правка реплика-поля Talent-реплики (sync-guard, аналог INV-BLD-7)«Поле управляется синхронизацией Talent»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Version не совпал«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет permission chessboard.property-objects.manage«Недостаточно прав»no retry

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

  • Новые optional поля тела — additive; изменение статуса/цены — отдельные операции, не этот метод.
  • Logs: TenantId, PropertyObjectId, изменённые поля, флаг TotalAreaChanged; metrics: chessboard_developers_property_object_update_total{result}, chessboard_property_object_price_per_meter_recalc_total; traces — OTel (traceparent); dashboard chessboard-overview.

Acceptance Criteria

  • AC1 (паспортная правка + пересчёт цены/м²). Дано: объект SyncSource = Pin, актуальный Version. Когда: меняется TotalArea. Тогда: PricePerMeter = round(Price / TotalArea, 2) (сам Price не изменён), Version +1, публикуется pin.chessboard.property-object-upserted.v1; price- и status-события не публикуются.
  • AC2 (правка при активном холде). Дано: объект в Reserved/Booked. Когда: правятся паспортные поля. Тогда: правка проходит (200), Status/таймеры/Price не тронуты.
  • AC3 (sync-guard реплики). Дано: SyncSource = TalentChessboard. Когда: реплика-поле (Number/Floor/…/LayoutId) отличается. Тогда: CHESSBOARD_PROPERTY_OBJECT_FIELD_SYNC_MANAGED (409); PIN-поля (CeilingHeightMeters/Capacity/Description) применяются.
  • AC4 (дубль номера). Дано: SyncSource = Pin, номер занят другим объектом здания. Когда: смена номера. Тогда: CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN (409).
  • AC5 (планировка другого проекта). Дано: LayoutId из другого проекта или архивная. Когда: привязка. Тогда: CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH (409).
  • AC6 (этаж/секция вне габаритов). Дано: SyncSource = Pin. Когда: Floor/Section вне габаритов здания (INV-BLD-3). Тогда: ValidationError (400).
  • AC7 (пересчёт счётчиков планировок). Дано: объект перепривязан к другой планировке. Когда: смена LayoutId. Тогда: запрошен project-aggregates-recalc; Layout.PropertyObjectsCount старой и новой планировки сходятся после окна debounce.
  • AC8 (конкурентная правка). Дано: устаревший Version. Когда: сохранение. Тогда: CHESSBOARD_CONCURRENCY_CONFLICT (409); проигравший перечитывает и повторяет.
  • AC9 (чужой tenant). Дано: объект другого застройщика. Когда: правка. Тогда: CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND (404), событие не публикуется.

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

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

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