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). Дом полей —
сущность Объект недвижимости.
| Поле | Значение |
|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | propertyObjects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/update-property-object.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.property-objects.manage |
| Scope (RBAC) | организация / группа проектов / проект |
| Tenant isolation | TenantId = CompanyId из JWT; чужой объект недвижимости → 404 |
| Audit | обновление: UpdatedByUserId, инкремент Version |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/v1/developers/property-objects/{propertyObjectId} |
| Success status | 200 |
| Idempotency header | не применяется (PUT + Version) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Признак «реплика-поле» = поле, владелец которого Talent Chessboard при SyncSource = TalentChessboard
(перечень — поле SyncSource в property-object.md); правка блокируется
sync-guard-ом (шаг 2). Признак «PIN-поле» = локальное поле PIN, редактируемо всегда.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Признак |
|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
PropertyObjectId | route | Guid | Да | существующий объект недвижимости tenant-а | 404 при чужом |
Number | body | string | Да | 1..32; уникален в здании (INV-CHS-006) | реплика-поле |
Floor | body | short | Да | -5..200; <= Building.FloorsTotal (INV-BLD-3) | реплика-поле |
Section | body | short? | Нет | 1..50; <= Building.SectionsCount | реплика-поле |
Rooms | body | short | Да | 0..20; 0 = студия | реплика-поле |
TotalArea | body | decimal | Да | > 0, numeric(8,2); смена → пересчёт PricePerMeter | реплика-поле |
LivingArea | body | decimal? | Нет | > 0 и <= TotalArea, numeric(8,2) | реплика-поле |
CeilingHeightMeters | body | decimal? | Нет | 2..10, numeric(4,2) | PIN-поле |
LayoutId | body | Guid? | Нет | планировка того же проекта, не архивная (INV-CHS-007) | реплика-поле (привязка) |
WindowViews | body | PropertyObjectWindowViewsInput | Да (может быть пустой) | typed-модель ниже | реплика-поле |
Capacity | body | short? | Нет | 1..20 (апарт-отели) | PIN-поле |
Description | body | string? | Нет | 0..4000 | PIN-поле |
Version | body | long | Да | >= 1; optimistic-сверка с PropertyObjectEntity.Version | — |
Зеркало PropertyObjectWindowViewsDocument (property-object.md);
ContractVersion проставляет сервер (= 1).
| Поле | Тип | Обязательность | Ограничения |
|---|
Views | IReadOnlyList<WindowViewKind> (enum-строки) | Да (может быть пустым) | значения WindowViewKind (reference-data.md); ≤ 4, без дублей |
PanoramaUrl | string? | Нет | varchar(512), URL 3D-тура/панорамы |
Модель ответа UpdateDeveloperPropertyObjectResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | PropertyObjectEntity.Id | — | Id объекта недвижимости. |
Version | long | Да | PropertyObjectEntity.Version (после инкремента) | — | Новая доменная версия. |
PricePerMeter | decimal | Да | PropertyObjectEntity.PricePerMeter | — | Цена за м² (пересчитана при смене TotalArea). |
UpdatedAt | DateTimeOffset | Да | 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. Компенсаций не требуется — снапшот полный, идемпотентен.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.property-objects.manage.
- Проверка входа и предусловий.
- формат/диапазоны полей — иначе
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).
- Применение правок. Набор мутируемых полей зависит от
SyncSource: всегда — PIN-поля (высота
потолков, вместимость, описание); при SyncSource = Pin дополнительно — реплика-поля (номер, этаж,
секция, комнатность, площади, виды из окон ContractVersion = 1, привязка планировки). При изменении
TotalArea пересчитывается PricePerMeter = round(Price / TotalArea, 2) (сам Price не меняется —
price-события нет). Проставляются UpdatedByUserId, инкремент Version.
- Факты и события. Публикуется событие
pin.chessboard.property-object-upserted.v1 — полный паспортный
снапшот после правки; новое PricePerMeter доставляется потребителям в этом снапшоте. Событие цены и
события статуса не публикуются. При смене LayoutId запрашивается фоновый пересчёт агрегатов
(project-aggregates-recalc, debounce 30 с) — Layout.PropertyObjectsCount старой и новой планировки.
- Результат. Возвращаются
Id, Version, PricePerMeter, UpdatedAt; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | PropertyObjectUpsertedKafkaEvent → pin.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 |
|---|
ValidationError | 400 | Нарушение format/min/max/enum, Floor/Section вне габаритов здания (INV-BLD-3) | Список ошибок полей | no retry |
CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND | 404 | Нет объекта недвижимости / чужой tenant | «Объект недвижимости не найден» | no retry |
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN | 409 | Дубль номера в здании (INV-CHS-006) | «Номер объекта занят» | no retry |
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH | 409 | Планировка чужого проекта/архивная (INV-CHS-007) | «Планировка недоступна для этого объекта недвижимости» | no retry |
CHESSBOARD_PROPERTY_OBJECT_FIELD_SYNC_MANAGED | 409 | Правка реплика-поля Talent-реплики (sync-guard, аналог INV-BLD-7) | «Поле управляется синхронизацией Talent» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Version не совпал | «Данные изменены параллельно» | retry после перечитывания |
FORBIDDEN | 403 | Нет 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), событие не публикуется.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы