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

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 areadevelopers
Feature grouppropertyObjects
Статусdraft
Документdocs/06-services/chessboard/api/developers/change-property-object-price.md

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/property-objects/{propertyObjectId}/price
Success status200
Idempotency headerне применяется — естественная идемпотентность: NewPrice == Price → no-op
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredactor-stamping
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation (ADR-0052)
PropertyObjectIdrouteGuidДасуществующий объект недвижимости tenant-а404 при чужом
NewPricebodydecimalДа>= 0; масштаб numeric(14,2)
RowVersionbodyuint?Нетoptimistic-guard; расхождение → 409

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

ПолеТипОбязательностьИсточникMaskingОписание
PropertyObjectIdGuidДаPropertyObjectEntity.IdId объекта недвижимости.
OldPricedecimalДаPrice до мутацииПрежняя цена (для UI-diff).
PricedecimalДаPropertyObjectEntity.Price (= NewPrice)Новая цена.
CurrencyIdshortДаPropertyObjectEntity.CurrencyIdВалюта — ссылка на справочник handbook (валюта проекта, INV-PRJ-6; RUB-only, ADR-0048).
PricePerMeterdecimalДаPropertyObjectEntity.PricePerMeterround(Price / TotalArea, 2).
StatusPropertyObjectStatusДаPropertyObjectEntity.StatusНе изменяется операцией (см. INV-CHS-001).
VersionlongДаPropertyObjectEntity.VersionНовая доменная версия.
UpdatedAtDateTimeOffsetДаPropertyObjectEntity.UpdatedAtМомент изменения.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.property-objects.manage в scope проекта.
  2. Проверка входа и предусловий.
    • PropertyObjectId != Guid.Empty, NewPrice >= 0, масштаб numeric(14,2) — иначе ValidationError (400);
    • объект недвижимости существует в пределах tenant-а — иначе CHESSBOARD_PROPERTY_OBJECT_NOT_FOUND (404);
    • статус ≠ Sold (INV-CHS-008); SoldCHESSBOARD_PROPERTY_OBJECT_PRICE_LOCKED (409); Reserved/Booked допустимы (шаг 3, предупреждение);
    • при заданном RowVersion — совпадение с текущей версией строки, иначе CHESSBOARD_CONCURRENCY_CONFLICT (409, гонка «правка цены vs CRM-событие статуса»).
  3. Смена цены. No-op guard: NewPrice == Price → возврат без изменений и событий (естественная идемпотентность). Иначе Price := NewPrice, PricePerMeter = round(NewPrice / TotalArea, 2); Status/StatusSource/таймеры не трогаются; проставляются UpdatedByUserId, инкремент Version. Валюта зафиксирована на объекте (INV-PRJ-6). При Status ∈ {Reserved, Booked} цена меняется, но фиксируется предупреждение (цена активной сделки фиксируется CRM отдельно — граница ответственности).
  4. Факты и события. Публикуется событие 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 с).
  5. Результат. Возвращаются PropertyObjectId, OldPrice, Price, CurrencyId, PricePerMeter, Status, Version, UpdatedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияPropertyObjectPriceChangedKafkaEventpin.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_FOUND404Объект недвижимости не существует / чужой tenant«Объект не найден»no retry (обновить список объектов недвижимости)
CHESSBOARD_PROPERTY_OBJECT_PRICE_LOCKED409INV-CHS-008: Status = Sold — цена проданного объекта недвижимости фиксирована«Объект продан, цена зафиксирована»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Конфликт версии — параллельная правка / CRM-событие«Данные изменены параллельно»retry после перечитывания объекта недвижимости
ValidationError400NewPrice < 0 или превышение precision numeric(14,2)«Проверьте значение цены»no retry
FORBIDDEN403Нет chessboard.property-objects.manage в scope проекта«Недостаточно прав»no retry

Сквозные коды области (UNAUTHORIZED 401 и др.) — README.md, не дублируются.

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

  • Контракт стабилен; RowVersion — additive optional (клиенты без optimistic-guard работают на last-write-wins в пределах транзакции). Изменение/удаление полей ответа или события — breaking (bump ContractVersion, новый топик, dual-publish), см. ../events/property-object-price-changed.md.
Синхронизация Talent

Смена цены синком 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 с).

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

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

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