POST /api/v1/developers/buildings/{buildingId}/property-objects — Создание объекта недвижимости
Назначение
Менеджер застройщика заводит ячейку шахматки в здании своей компании: номер, этаж/секция (оси Y/X),
комнатность, площади, цену, привязку к планировке. Объект создаётся вручную в PIN (не из синка Talent) в
статусе Free (SyncSource = Pin, StatusSource = DeveloperApi), реплика-поля не заполняются. Домен полей —
сущность Объект недвижимости.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | developers |
| Feature group | propertyObjects |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/developers/create-property-object.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (менеджер застройщика) |
| Auth policy / Permissions | chessboard.property-objects.manage |
| Scope (RBAC) | организация / группа проектов / проект (проект здания в scope) |
| Tenant isolation | TenantId = CompanyId из JWT; PropertyObject.TenantId наследуется от проекта = Building.TenantId (INV-CHS-004) |
| Audit | создание: CreatedByUserId, Version = 1, StatusChangedAt |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/developers/buildings/{buildingId}/property-objects |
| Success status | 201 (Location: /api/v1/developers/property-objects/{id}) |
| Idempotency header | Idempotency-Key (опционально) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | — |
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
BuildingId | route | Guid | Да | существующее здание tenant-а, не удалено | 404 при чужом/отсутствии |
Number | body | string | Да | 1..32, trim; уникально в здании среди не удалённых (INV-CHS-006) | — |
Floor | body | short | Да | -5..200; кросс-проверка <= Building.FloorsTotal (INV-BLD-3) | — |
Section | body | short? | Нет | 1..50; при заданном — Building.SectionsCount != null и <= SectionsCount | — |
Rooms | body | short | Да | 0..20 (0 = студия) | — |
TotalArea | body | decimal | Да | > 0, precision (8,2) | — |
LivingArea | body | decimal? | Нет | > 0 и <= TotalArea, precision (8,2) | — |
CeilingHeightMeters | body | decimal? | Нет | 2..10, precision (4,2) | — |
Price | body | decimal | Да | >= 0, precision (14,2) | — |
LayoutId | body | Guid? | Нет | существующая планировка того же проекта (INV-CHS-007) | 404 при чужой |
WindowViews | body | IReadOnlyList<WindowViewKind> | Нет (default пустой) | enum-строки Sea/City/Courtyard/Mountains/Park/River/Street; ≤ 4, без дублей | — |
Capacity | body | short? | Нет | 1..20 (апарт-отели) | — |
Description | body | string? | Нет | 0..4000 | — |
Поля, не принимаемые в запросе
CurrencyId не принимается — берётся из Project.CurrencyId (INV-PRJ-6). Status, таймеры (ReservedUntil/
BookedUntil), HoldDealId, PricePerMeter не принимаются: статус фиксирован Free, таймеры пусты
(INV-CHS-002), цена за м² вычисляется. Реплика-поля (ExternalTalentPropertyObjectId, ReplicaVersion) —
null.
Модель ответа CreateDeveloperPropertyObjectResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PropertyObjectEntity.Id | — | Id созданного объекта недвижимости. |
BuildingId | Guid | Да | BuildingId | — | Здание. |
ProjectId | Guid | Да | PropertyObjectEntity.ProjectId | — | Проект (денормализация от здания). |
Status | PropertyObjectStatus | Да | Status | — | Всегда Free. |
Number | string | Да | Number | — | Присвоенный номер. |
CurrencyId | short | Да | CurrencyId | — | Валюта — ссылка на справочник handbook, унаследована от проекта (INV-PRJ-6; RUB-only, ADR-0048). |
PricePerMeter | decimal | Да | PricePerMeter | — | Вычислено round(Price / TotalArea, 2). |
Version | long | Да | Version | — | 1. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер застройщика; tenant — его компания; требуется право
chessboard.property-objects.manageв scope проекта здания. При заголовкеIdempotency-Keyповтор возвращает сохранённыйIdбез повторной вставки и без повторного события; тот же ключ с иным телом →IDEMPOTENCY_KEY_MISMATCH(409). - Проверка входа и предусловий.
- формат/диапазоны полей (
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 indexux_property_objects_building_number; - если задан
LayoutId— планировка существует (иначеCHESSBOARD_LAYOUT_NOT_FOUND, 404) и принадлежит тому же проекту (INV-CHS-007), иначеCHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH(409).
- формат/диапазоны полей (
- Чтение контекста. Здание — источник
ProjectIdи границ этажей/секций; проект — источникTenantId(INV-CHS-004) иCurrencyId(INV-PRJ-6). СогласованностьBuilding.ProjectId == Project.Id(INV-CHS-005) выполняется по построению; рассинхрон данных →CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH(409). - Создание объекта. Объект заводится со статусом
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. - Факты и события. Публикуется интеграционное событие
pin.chessboard.property-object-upserted.v1(паспортный снапшот). Запрашивается фоновый пересчёт агрегатов проекта/здания/планировки (project-aggregates-recalc, debounce 30 с) —Project.Price,PropertyObjectsTotalCount/FreeCount,Building.PropertyObjects*Count,Layout.PropertyObjectsCount. - Результат. Возвращаются паспортные поля объекта (
Id,BuildingId,ProjectId,Status = Free,Number,CurrencyId,PricePerMeter,Version = 1,CreatedAt); HTTP201+Location.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | PropertyObjectUpsertedKafkaEvent → pin.chessboard.property-object-upserted.v1; потребители: catalog (проекция витрины), dev-stats (измерение PropertyObject), rewards, invest-analytics |
| Проекции / пересчёт | ProjectAggregatesRecalcRequestedEvent → job project-aggregates-recalc (пересчёт агрегатов проекта/здания/планировки) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение ограничений полей (Number/Rooms/площади/Price/WindowViews) | Список ошибок полей | no retry |
CHESSBOARD_BUILDING_NOT_FOUND | 404 | Здание не найдено / чужого tenant-а / удалено | «Здание не найдено» | no retry |
CHESSBOARD_PROPERTY_OBJECT_FLOOR_OUT_OF_BUILDING | 409 | Floor > Building.FloorsTotal (INV-BLD-3) | «Этаж вне диапазона здания» | no retry |
CHESSBOARD_PROPERTY_OBJECT_SECTION_OUT_OF_BUILDING | 409 | Section задана без секций здания или > SectionsCount | «Секция вне диапазона здания» | no retry |
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN | 409 | Дубль номера в здании (INV-CHS-006) | «Номер объекта недвижимости занят» | no retry |
CHESSBOARD_LAYOUT_NOT_FOUND | 404 | LayoutId задан, планировка не найдена | «Планировка не найдена» | no retry |
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH | 409 | Планировка принадлежит другому проекту (INV-CHS-007) | «Планировка из другого проекта» | no retry |
CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH | 409 | Building.ProjectId != Project.Id (INV-CHS-005; рассинхрон данных) | «Здание не принадлежит проекту» | no retry |
IDEMPOTENCY_KEY_MISMATCH | 409 | Тот же Idempotency-Key переиспользован с иным телом запроса | «Ключ идемпотентности переиспользован» | no retry |
FORBIDDEN | 403 | Нет 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); dashboardchessboard-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 с) и потребления события — до этого публичные счётчики отстают.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): Chessboard API — реестр операций областей Developers и Admin, POST /api/v1/developers/buildings/{buildingId}/property-objects/bulk — Массовый upsert объектов недвижимости
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: ../domain/property-object.md (INV-CHS-004/005/006/007, INV-CHS-002); родители — ../domain/building.md (INV-BLD-3), ../domain/project.md (INV-PRJ-6); планировка — ../domain/layout.md; enum-ы — ../domain/reference-data.md.
- Событие: ../events/property-object-upserted.md.
- Соседние операции: create-building.md; реестр — README.md.