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

POST /api/v1/developers/buildings/{buildingId}/property-objects — Создание объекта недвижимости

Назначение

Менеджер застройщика заводит ячейку шахматки в здании своей компании: номер, этаж/секция (оси Y/X), комнатность, площади, цену, привязку к планировке. Объект создаётся вручную в PIN (не из синка Talent) в статусе Free (SyncSource = Pin, StatusSource = DeveloperApi), реплика-поля не заполняются. Домен полей — сущность Объект недвижимости.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (менеджер застройщика)
Auth policy / Permissionschessboard.property-objects.manage
Scope (RBAC)организация / группа проектов / проект (проект здания в scope)
Tenant isolationTenantId = CompanyId из JWT; PropertyObject.TenantId наследуется от проекта = Building.TenantId (INV-CHS-004)
Auditсоздание: CreatedByUserId, Version = 1, StatusChangedAt

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developers/buildings/{buildingId}/property-objects
Success status201 (Location: /api/v1/developers/property-objects/{id})
Idempotency headerIdempotency-Key (опционально)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
BuildingIdrouteGuidДасуществующее здание tenant-а, не удалено404 при чужом/отсутствии
NumberbodystringДа1..32, trim; уникально в здании среди не удалённых (INV-CHS-006)
FloorbodyshortДа-5..200; кросс-проверка <= Building.FloorsTotal (INV-BLD-3)
Sectionbodyshort?Нет1..50; при заданном — Building.SectionsCount != null и <= SectionsCount
RoomsbodyshortДа0..20 (0 = студия)
TotalAreabodydecimalДа> 0, precision (8,2)
LivingAreabodydecimal?Нет> 0 и <= TotalArea, precision (8,2)
CeilingHeightMetersbodydecimal?Нет2..10, precision (4,2)
PricebodydecimalДа>= 0, precision (14,2)
LayoutIdbodyGuid?Нетсуществующая планировка того же проекта (INV-CHS-007)404 при чужой
WindowViewsbodyIReadOnlyList<WindowViewKind>Нет (default пустой)enum-строки Sea/City/Courtyard/Mountains/Park/River/Street; ≤ 4, без дублей
Capacitybodyshort?Нет1..20 (апарт-отели)
Descriptionbodystring?Нет0..4000
Поля, не принимаемые в запросе

CurrencyId не принимается — берётся из Project.CurrencyId (INV-PRJ-6). Status, таймеры (ReservedUntil/ BookedUntil), HoldDealId, PricePerMeter не принимаются: статус фиксирован Free, таймеры пусты (INV-CHS-002), цена за м² вычисляется. Реплика-поля (ExternalTalentPropertyObjectId, ReplicaVersion) — null.

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPropertyObjectEntity.IdId созданного объекта недвижимости.
BuildingIdGuidДаBuildingIdЗдание.
ProjectIdGuidДаPropertyObjectEntity.ProjectIdПроект (денормализация от здания).
StatusPropertyObjectStatusДаStatusВсегда Free.
NumberstringДаNumberПрисвоенный номер.
CurrencyIdshortДаCurrencyIdВалюта — ссылка на справочник handbook, унаследована от проекта (INV-PRJ-6; RUB-only, ADR-0048).
PricePerMeterdecimalДаPricePerMeterВычислено round(Price / TotalArea, 2).
VersionlongДаVersion1.
CreatedAtDateTimeOffsetДаCreatedAtМомент создания.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право chessboard.property-objects.manage в scope проекта здания. При заголовке Idempotency-Key повтор возвращает сохранённый Id без повторной вставки и без повторного события; тот же ключ с иным телом → IDEMPOTENCY_KEY_MISMATCH (409).
  2. Проверка входа и предусловий.
    • формат/диапазоны полей (Number, Rooms 0..20, TotalArea > 0, LivingArea <= TotalArea, Price >= 0, состав WindowViews) — иначе ValidationError;
    • здание-родитель существует в пределах tenant-а — иначе CHESSBOARD_BUILDING_NOT_FOUND (404);
    • Floor <= Building.FloorsTotal (INV-BLD-3) — иначе CHESSBOARD_PROPERTY_OBJECT_FLOOR_OUT_OF_BUILDING (409); при заданной Section — здание имеет секции и Section <= Building.SectionsCount, иначе CHESSBOARD_PROPERTY_OBJECT_SECTION_OUT_OF_BUILDING (409);
    • номер уникален в здании среди не удалённых (INV-CHS-006) — иначе CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN (409); гонка добивается unique index ux_property_objects_building_number;
    • если задан LayoutId — планировка существует (иначе CHESSBOARD_LAYOUT_NOT_FOUND, 404) и принадлежит тому же проекту (INV-CHS-007), иначе CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH (409).
  3. Чтение контекста. Здание — источник ProjectId и границ этажей/секций; проект — источник TenantId (INV-CHS-004) и CurrencyId (INV-PRJ-6). Согласованность Building.ProjectId == Project.Id (INV-CHS-005) выполняется по построению; рассинхрон данных → CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH (409).
  4. Создание объекта. Объект заводится со статусом Free ([*] → Free, StatusSource = DeveloperApi, StatusChangedAt = now), таймеры и HoldDealId пусты (INV-CHS-002); SyncSource = Pin. Поля берутся из формы; CurrencyId — из проекта (INV-PRJ-6); PricePerMeter = round(Price / TotalArea, 2); WindowViews — typed-модель (ContractVersion = 1); проставляются CreatedByUserId, Version = 1.
  5. Факты и события. Публикуется интеграционное событие pin.chessboard.property-object-upserted.v1 (паспортный снапшот). Запрашивается фоновый пересчёт агрегатов проекта/здания/планировки (project-aggregates-recalc, debounce 30 с) — Project.Price, PropertyObjectsTotalCount/FreeCount, Building.PropertyObjects*Count, Layout.PropertyObjectsCount.
  6. Результат. Возвращаются паспортные поля объекта (Id, BuildingId, ProjectId, Status = Free, Number, CurrencyId, PricePerMeter, Version = 1, CreatedAt); HTTP 201 + Location.

Диаграмма

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

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нарушение ограничений полей (Number/Rooms/площади/Price/WindowViews)Список ошибок полейno retry
CHESSBOARD_BUILDING_NOT_FOUND404Здание не найдено / чужого tenant-а / удалено«Здание не найдено»no retry
CHESSBOARD_PROPERTY_OBJECT_FLOOR_OUT_OF_BUILDING409Floor > Building.FloorsTotal (INV-BLD-3)«Этаж вне диапазона здания»no retry
CHESSBOARD_PROPERTY_OBJECT_SECTION_OUT_OF_BUILDING409Section задана без секций здания или > SectionsCount«Секция вне диапазона здания»no retry
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN409Дубль номера в здании (INV-CHS-006)«Номер объекта недвижимости занят»no retry
CHESSBOARD_LAYOUT_NOT_FOUND404LayoutId задан, планировка не найдена«Планировка не найдена»no retry
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH409Планировка принадлежит другому проекту (INV-CHS-007)«Планировка из другого проекта»no retry
CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH409Building.ProjectId != Project.Id (INV-CHS-005; рассинхрон данных)«Здание не принадлежит проекту»no retry
IDEMPOTENCY_KEY_MISMATCH409Тот же Idempotency-Key переиспользован с иным телом запроса«Ключ идемпотентности переиспользован»no retry
FORBIDDEN403Нет permission chessboard.property-objects.manage«Недостаточно прав»no retry

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

  • Новые optional поля тела — additive (без breaking); удаление/переименование/смена типа — breaking (migration + rollout), см. ../events/property-object-upserted.md.
  • Logs: TenantId, ProjectId, BuildingId, PropertyObjectId (PII нет); metrics: chessboard_developers_property_object_create_total{result}, latency p95; traces — OTel (traceparent); dashboard chessboard-overview.

Acceptance Criteria

  • Happy path. Дано: менеджер застройщика с chessboard.property-objects.manage в scope проекта; здание tenant-а существует. Когда: POST с валидным телом. Тогда: создан объект Status = Free, StatusSource = DeveloperApi, SyncSource = Pin, таймеры пусты (INV-CHS-002), CurrencyId из проекта (INV-PRJ-6), PricePerMeter = round(Price/TotalArea, 2), Version = 1; 201 + Location; опубликовано pin.chessboard.property-object-upserted.v1 + запрошен пересчёт агрегатов.
  • Этаж/секция вне здания. Дано: Floor > Building.FloorsTotal (INV-BLD-3) или Section без секций здания / > SectionsCount. Когда: создание. Тогда: CHESSBOARD_PROPERTY_OBJECT_FLOOR_OUT_OF_BUILDING либо CHESSBOARD_PROPERTY_OBJECT_SECTION_OUT_OF_BUILDING (409).
  • Дубль номера. Дано: в здании уже есть не удалённый объект с тем же Number. Когда: создание (в т.ч. гонка). Тогда: CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN (409, INV-CHS-006), одиночка добивается unique index.
  • Планировка чужого проекта. Дано: LayoutId указывает на планировку другого проекта. Когда: создание. Тогда: CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH (409, INV-CHS-007); отсутствующая планировка → CHESSBOARD_LAYOUT_NOT_FOUND (404).
  • Здание чужое/удалено. Дано: buildingId другого tenant-а либо удалено. Когда: создание. Тогда: CHESSBOARD_BUILDING_NOT_FOUND (404).
  • Статус/валюта/таймеры не принимаются. Дано: тело содержит Status/CurrencyId/ReservedUntil и т.п. Когда: создание. Тогда: поля игнорируются — статус всегда Free, валюта из проекта, таймеры пусты (INV-CHS-001/002).
  • Идемпотентный повтор. Дано: первый вызов под Idempotency-Key = K. Когда: повтор с тем же K и телом → тот же Id; при ином теле → IDEMPOTENCY_KEY_MISMATCH (409).
  • Eventual consistency. После 201 объект виден в шахматке кабинета как Free сразу; счётчики проекта/ здания/планировки (PropertyObjects*Count) и публичная лента/витрина catalog обновляются после пересчёта агрегатов (debounce 30 с) и потребления события — до этого публичные счётчики отстают.

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

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

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