POST /api/v1/developers/property-objects/{propertyObjectId}/price — Изменение цены объекта недвижимости
Назначение
Менеджер застройщика корректирует текущую цену объекта недвижимости: Price := NewPrice с пересчётом
PricePerMeter. По изменению публикуется событие pin.chessboard.property-object-price-changed.v1
(append-only история цен invest-analytics, обновление витрины catalog) и запрашивается пересчёт
денормализованного диапазона цен проекта. Статус объекта операцией не меняется (переходы статуса — только
события CRM / job истечения, INV-CHS-001). Сценарий-источник —
UC-CHS-003; дом полей и инварианты —
сущность Объект недвижимости (INV-CHS-008).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | propertyObjects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/change-property-object-price.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.property-objects.manage |
| Scope (RBAC) | организация / группа проектов / проект |
| Tenant isolation | TenantId = CompanyId из JWT; чужой объект недвижимости → 404 |
| Audit | обновление: UpdatedByUserId, инкремент Version; история цены восстанавливается из событий |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/property-objects/{propertyObjectId}/price |
| Success status | 200 |
| Idempotency header | не применяется — естественная идемпотентность: NewPrice == Price → no-op |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | actor-stamping |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation (ADR-0052) |
PropertyObjectId | route | Guid | Да | существующий объект недвижимости tenant-а | 404 при чужом |
NewPrice | body | decimal | Да | >= 0; масштаб numeric(14,2) | — |
RowVersion | body | uint? | Нет | optimistic-guard; расхождение → 409 | — |
Модель ответа ChangeDeveloperPropertyObjectPriceResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | PropertyObjectEntity.Id | — | Id объекта недвижимости. |
OldPrice | decimal | Да | Price до мутации | — | Прежняя цена (для UI-diff). |
Price | decimal | Да | PropertyObjectEntity.Price (= NewPrice) | — | Новая цена. |
CurrencyId | short | Да | PropertyObjectEntity.CurrencyId | — | Валюта — ссылка на справочник handbook (валюта проекта, INV-PRJ-6; RUB-only, ADR-0048). |
PricePerMeter | decimal | Да | PropertyObjectEntity.PricePerMeter | — | round(Price / TotalArea, 2). |
Status | PropertyObjectStatus | Да | PropertyObjectEntity.Status | — | Не изменяется операцией (см. INV-CHS-001). |
Version | long | Да | PropertyObjectEntity.Version | — | Новая доменная версия. |
UpdatedAt | DateTimeOffset | Да | PropertyObjectEntity.UpdatedAt | — | Момент изменения. |
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.property-objects.manageв scope проекта. - Проверка входа и предусловий.
PropertyObjectId != Guid.Empty,NewPrice >= 0, масштабnumeric(14,2)— иначеValidationError(400);- объект недвижимости существует в пределах tenant-а — иначе
CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND(404); - статус ≠
Sold(INV-CHS-008);Sold→CHESSBOARD_PROPERTY_OBJECT_PRICE_LOCKED(409);Reserved/Bookedдопустимы (шаг 3, предупреждение); - при заданном
RowVersion— совпадение с текущей версией строки, иначеCHESSBOARD_CONCURRENCY_CONFLICT(409, гонка «правка цены vs CRM-событие статуса»).
- Смена цены. No-op guard:
NewPrice == Price→ возврат без изменений и событий (естественная идемпотентность). ИначеPrice := NewPrice,PricePerMeter = round(NewPrice / TotalArea, 2);Status/StatusSource/таймеры не трогаются; проставляютсяUpdatedByUserId, инкрементVersion. Валюта зафиксирована на объекте (INV-PRJ-6). ПриStatus ∈ {Reserved, Booked}цена меняется, но фиксируется предупреждение (цена активной сделки фиксируется CRM отдельно — граница ответственности). - Факты и события. Публикуется событие
pin.chessboard.property-object-price-changed.v1(payload:PropertyObjectId,TenantId,ProjectId,OldPrice,NewPrice,CurrencyId+CurrencySnapshot(snapshots.md),PricePerMeter,ChangedAt,Source = DeveloperApi,Version). Паспортное событиеproperty-object-upsertedне публикуется — цена не паспортное поле. Запрашивается фоновый пересчётProject.Price(project-aggregates-recalc, debounce 30 с). - Результат. Возвращаются
PropertyObjectId,OldPrice,Price,CurrencyId,PricePerMeter,Status,Version,UpdatedAt; HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | PropertyObjectPriceChangedKafkaEvent → pin.chessboard.property-object-price-changed.v1; потребители: invest-analytics (append-only история цен, график роста), catalog (цена витрины/шахматки) |
| Проекции / пересчёт | ProjectAggregatesRecalcRequestedEvent → job project-aggregates-recalc (пересчёт Project.Price, debounce 30 с); предупреждение при Reserved/Booked |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND | 404 | Объект недвижимости не существует / чужой tenant | «Объект не найден» | no retry (обновить список объектов недвижимости) |
CHESSBOARD_PROPERTY_OBJECT_PRICE_LOCKED | 409 | INV-CHS-008: Status = Sold — цена проданного объекта недвижимости фиксирована | «Объект продан, цена зафиксирована» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Конфликт версии — параллельная правка / CRM-событие | «Данные изменены параллельно» | retry после перечитывания объекта недвижимости |
ValidationError | 400 | NewPrice < 0 или превышение precision numeric(14,2) | «Проверьте значение цены» | no retry |
FORBIDDEN | 403 | Нет chessboard.property-objects.manage в scope проекта | «Недостаточно прав» | no retry |
Сквозные коды области (UNAUTHORIZED 401 и др.) — README.md, не дублируются.
Совместимость и наблюдаемость
- Контракт стабилен;
RowVersion— additive optional (клиенты без optimistic-guard работают на last-write-wins в пределах транзакции). Изменение/удаление полей ответа или события — breaking (bumpContractVersion, новый топик, dual-publish), см. ../events/property-object-price-changed.md.
Смена цены синком Talent идёт той же операцией со Source = TalentSync (UC-CHS-003 A3) — история цен в
invest-analytics едина, различается полем Source.
- Logs:
TenantId,PropertyObjectId,OldPrice → NewPrice,Source(цена — не PII); metrics:chessboard_property_object_price_changed_total,chessboard_price_changed_under_hold_total{status}; traces — OTel (traceparent).
Acceptance Criteria
- Happy path. Дано: менеджер застройщика с
chessboard.property-objects.manage; объект tenant-а в статусеFree,NewPrice != Price. Когда:POST …/price. Тогда:Price := NewPrice,PricePerMeter = round(NewPrice/TotalArea, 2), статус и таймеры не тронуты,Versionувеличен;200; опубликованоpin.chessboard.property-object-price-changed.v1(Source = DeveloperApi) + запрошен пересчётProject.Price. - No-op при равной цене. Дано:
NewPrice == Price. Когда: вызов. Тогда:200без изменений, без события и без пересчёта (естественная идемпотентность). - Продан — цена заблокирована. Дано:
Status = Sold. Когда: вызов. Тогда:CHESSBOARD_PROPERTY_OBJECT_PRICE_LOCKED(409, INV-CHS-008); цена не меняется, событие не публикуется. - Активная бронь. Дано:
Status ∈ {Reserved, Booked}. Когда: вызов с новой ценой. Тогда: цена меняется, событие публикуется, дополнительно фиксируется предупреждение (цена сделки фиксируется CRM отдельно). - Конкурентный конфликт. Дано: передан
RowVersion, но версия строки уже изменена параллельной правкой или CRM-событием статуса. Когда: вызов. Тогда:CHESSBOARD_CONCURRENCY_CONFLICT(409); проигравший перечитывает объект и повторяет. - Невалидная цена. Дано:
NewPrice < 0или превышениеnumeric(14,2). Когда: вызов. Тогда:ValidationError(400). - Eventual consistency. После
200новая цена видна в кабинете сразу; точка истории цен вinvest-analyticsи цена витрины/шахматки вcatalogпоявляются после потребления события; диапазонProject.Price— после фонового пересчёта (debounce 30 с).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/buildings/{buildingId}/property-objects/bulk — Массовый upsert объектов недвижимости, PUT /api/v1/developers/property-objects/{propertyObjectId} — Обновление объекта недвижимости (паспортные поля)
- События (1): Event Contract: pin.chessboard.property-object-price-changed (PropertyObjectPriceChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/property-object.md (INV-CHS-008,
PricePerMeter);Source— enumPropertyObjectStatusSource(../domain/reference-data.md). - Событие: ../events/property-object-price-changed.md; соседнее — ../events/property-object-upserted.md (паспортный snapshot, не цена).
- Проект (диапазон цен): ../domain/project.md (
Price—NullableRange<decimal>); job — ../background/project-aggregates-recalc.md. - Сценарий: UC-CHS-003; реестр: README.md.
- История цен (чужой контекст):
docs/06-services/invest-analytics/consumers/chessboard-property-object-price-changed.md.