UC-CHS-002. Добавление здания (литера/корпуса) и объектов проекта застройщиком
Назначение
Менеджер застройщика наполняет проект структурой: добавляет здание (литер/корпус: этажность, секции,
срок сдачи, готовность), при необходимости заводит планировки и группы планировок, затем создаёт объекты
недвижимости (номер, этаж, секция, комнатность, площади, цена, вид из окна, вместимость) — будущие ячейки
шахматки. Сценарий относится к контексту chessboard.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CHS-002 |
| Контекст | chessboard (см. docs/06-services/chessboard/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Девелопер (менеджер застройщика) |
| Permissions / scope | chessboard.buildings.manage + chessboard.property-objects.manage + chessboard.layouts.manage; scope = организация / группа проектов / проект |
| Tenant | TenantId = CompanyId застройщика (ADR-0052); наследуется дочерними сущностями от проекта (INV-BLD-1, INV-CHS-004, INV-LAY-1, INV-LGR-1) |
| Record-level | Не применяется |
Предусловие
- Проект существует и принадлежит tenant-у актора (UC-CHS-001); статус проекта — любой не-
Archived(структуру можно наполнять и вDraft). - Для привязки объекта недвижимости к планировке — планировка того же проекта заведена (этим же сценарием, шаг 4).
Триггер
| Поле | Значение |
|---|---|
| Источник | Действие пользователя в кабинете застройщика |
| Триггерящий элемент | Кабинет застройщика → карточка проекта → «Добавить корпус» / «Добавить планировку» / «Добавить объект» |
Основной Поток
Добавление и публикация здания
- Менеджер создаёт здание (
POST /developers/projects/{projectId}/buildings):Name(«Литер 3»),FloorsTotal,SectionsCount,DeliveryYear+DeliveryQuarter,ConstructionStatus,ReadinessPercent,CeilingHeightMeters.- Вход:
Name1..128;FloorsTotal1..200;SectionsCount1..50;DeliveryYear2000..2100,DeliveryQuarter1..4 (обязателен при годе);ReadinessPercent0..100;ConstructionStatus— из reference-data.md. - Имя корпуса уникально в проекте (по нижнему регистру); занято →
CHESSBOARD_BUILDING_NAME_TAKEN(INV-BLD-2). Повтор ловится и unique-индексом имени. - Проект должен существовать, принадлежать актору и быть не
Archived. - Создаётся здание:
Status = Draft,SyncSource = Pin,TenantIdунаследован от проекта (INV-BLD-1); фиксируетсяBuildingCreatedEvent, публикуетсяBuildingUpsertedKafkaEvent(топикpin.chessboard.building-upserted.v1). Результат —201 Created.
- Вход:
- Менеджер публикует здание (
POST /developers/buildings/{id}/publish): guard INV-BLD-4 —Project.Status in (Draft, Published), заполненыFloorsTotal,DeliveryYear+DeliveryQuarter; иначеCHESSBOARD_BUILDING_PUBLISH_INCOMPLETE. ПереходDraft → Published, публикуетсяBuildingUpsertedKafkaEvent.
Планировки и группы (опционально)
- Менеджер заводит группу планировок (
POST /developers/projects/{projectId}/layout-groups:Name,SortOrder— INV-LGR-1/2) и планировки (POST /developers/projects/{projectId}/layouts:Name,Rooms,TotalArea,LivingArea,Schemas— файлыLayoutSchemasDocument,LayoutGroupId— INV-LAY-1/2/3). При создании публикуютсяLayoutGroupUpsertedKafkaEvent/LayoutUpsertedKafkaEvent.
Добавление объекта недвижимости
- Менеджер создаёт объект в корпусе (
POST /developers/buildings/{buildingId}/property-objects):Number,Floor,Section,Rooms,TotalArea,LivingArea,Price,LayoutId,WindowViews,Capacity(для отелей),CeilingHeightMeters,Description.- Вход:
Number1..32 и уникален в здании по нижнему регистру (INV-CHS-006);Floor−5..200 иFloor <= Building.FloorsTotal(INV-BLD-3);Section ≤ Building.SectionsCount;Rooms0..20;TotalArea > 0;LivingArea <= TotalArea;Price >= 0;Capacity1..20;WindowViews.Views≤ 4 значенияWindowViewKindбез дублей. - Проверки связей: здание существует и принадлежит актору;
Building.ProjectIdсогласован с проектом (INV-CHS-005), валюта объекта — ссылка справочникаCurrencyId, совпадающая сProject.CurrencyId(INV-PRJ-6); приLayoutIdпланировка существует и принадлежит тому же проекту (layout.ProjectId == building.ProjectId, INV-CHS-007). - Создаётся объект:
Status = Free,StatusSource = DeveloperApi,StatusChangedAt = now,CurrencyId = project.CurrencyId,PricePerMeter = round(Price / TotalArea, 2),SyncSource = Pin,ProjectId = building.ProjectId,BuildingId,TenantIdунаследован от здания (INV-CHS-004). ФиксируетсяPropertyObjectCreatedEvent(создание сразу вFree, статусное событие не поднимается) иProjectAggregatesRecalcRequestedEvent(ProjectId)— триггер пересчёта денормализаций. - Публикуется
PropertyObjectUpsertedKafkaEvent(топикpin.chessboard.property-object-upserted.v1, key =Id; паспортный снапшот — property-object.md). Результат —201 Created.
- Вход:
Пересчёт денормализаций
- Фоновый job
project-aggregates-recalc(debounce 30 с поProjectAggregatesRecalcRequestedEvent) пересчитываетProject.Price/PropertyObjectsTotalCount/PropertyObjectsFreeCount,Building.PropertyObjects*Count,Layout.PropertyObjectsCount,LayoutGroup.LayoutsCount/Area; изменение агрегатов проекта публикуетProjectUpsertedKafkaEventс новым диапазоном цен. - Шахматка проекта (UC-CHS-004) получает новую ячейку
Floor × Section; фронт обновляет дерево «корпуса → объекты».
Альтернативные Потоки
A1. Дубль номера объекта в здании
- Номер уже занят в здании (или гонку поймал unique-индекс
ux_property_objects_building_number) →CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN(INV-CHS-006). Менеджер меняет номер.
A2. Этаж выше этажности здания
Floor > Building.FloorsTotal→CHESSBOARD_BUILDING_FLOORS_CONFLICT(INV-BLD-3).- Обратная сторона: попытка уменьшить
FloorsTotalздания ниже максимального занятого этажа отклоняется тем же кодом.
A3. Планировка чужого проекта
layout.ProjectId != building.ProjectId→CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH(INV-CHS-007); аналогично для группы:CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH(INV-LAY-3).
A4. Скрытие/показ объекта недвижимости застройщиком
POST /developers/property-objects/{id}/hide: guard INV-CHS-009 — скрыть можно только изFree(Reserved/Booked/Soldскрыть нельзя →CHESSBOARD_PROPERTY_OBJECT_HIDE_FORBIDDEN); переходFree → Hidden,StatusSource = DeveloperApi, публикуетсяPropertyObjectStatusChangedKafkaEvent.POST .../unhide→Hidden → Free, тем же путём. Это единственные статусные переходы, доступные Developers API (INV-CHS-001).
A5. Правка паспортных полей объекта недвижимости-реплики Talent
- Для
SyncSource = TalentChessboardреплика-поля (Number,Floor,Section,Rooms, площади,WindowViews,LayoutId) правятся только синком; их правка через Developers API отклоняется (аналог INV-PRJ-9 для объекта недвижимости). PIN-поляDescription,Price— доступны.
A6. Здание/объект недвижимости в скрытом проекте
- Наполнение структуры допустимо при
Project.Status in (Draft, Published, Hidden); публичные проекции при этом фильтруются каскадом статусов (см. UC-CHS-004). ПриArchived— команда отклоняется на проверке проекта (404/409).
Постусловие
BuildingEntity(Status = Published) иPropertyObjectEntity[](Status = Free) существуют; tenant у всех унаследован от проекта;PricePerMeterвычислен.- Планировки/группы (если заводились) связаны с проектом;
PropertyObject.LayoutIdуказывает на планировку того же проекта. - Денормализации проекта/здания/планировок пересчитаны job-ом;
catalog,dev-stats,rewardsполучилиbuilding-upserted/property-object-upserted/layout-upserted/layout-group-upserted. - Шахматка проекта отображает новые ячейки (зелёные —
Free).
Интеграционная последовательность и согласованность
- Ячейка шахматки и агрегаты проекта обновляются в разное время. Создание объекта недвижимости сразу
публикует
property-object-upserted—catalogрисует новую зелёную ячейкуFloor × Sectionв пределах SLA (< 1 мин). ДенормализацииProject.Price/счётчики,Building.PropertyObjects*Count,Layout.PropertyObjectsCount,LayoutGroup.LayoutsCount/Areaпересчитывает jobproject-aggregates-recalcс debounce 30 с и публикует отдельныйproject-upserted. Промежуточно карточка проекта может показывать прежний диапазон цен/счётчики — расхождение закрывается в пределах окна debounce + SLA потребителя. - Схлопывание пересчёта. При массовом добавлении объектов недвижимости серия
ProjectAggregatesRecalcRequestedEventсхлопывается debounce-окном в один пересчёт проекта, а не N — job идемпотентен (пересчитывает из текущего состояния, а не инкрементом). - Частичный отказ. Если объекты недвижимости созданы (
*-upsertedдоставлены), но job пересчёта упал — объекты уже видны в шахматке, а агрегаты проекта устаревшие; повторный прогон job-а по накопленному запросу восстанавливает консистентность (пересчёт из актуального состояния). Отказ доставки события потребителю не откатывает создание объекта — согласованность достигается вперёд (eventual). - Порядок и version-guard. Паспортный
-upserted, статусный-status-changed(приHide/Unhide) и ценовой-price-changedживут в разных топиках; относительный порядок между ними не гарантирован — потребитель, ведущий единую реплику объекта недвижимости, держит общийVersion-guard поPropertyObjectId.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
CHESSBOARD_BUILDING_NAME_TAKEN | 409 | INV-BLD-2: имя корпуса занято в проекте | Предложить другое имя |
CHESSBOARD_BUILDING_PUBLISH_INCOMPLETE | 422 | INV-BLD-4: нет этажности/срока сдачи | Подсветить поля |
CHESSBOARD_BUILDING_FLOORS_CONFLICT | 422 | INV-BLD-3: этаж объекта недвижимости вне этажности (A2) | Поправить этаж/этажность |
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN | 409 | INV-CHS-006 (A1) | Поправить номер |
CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH | 422 | INV-CHS-005: здание другого проекта | Обновить форму (некорректный buildingId) |
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH | 422 | INV-CHS-007 (A3) | Выбрать планировку своего проекта |
CHESSBOARD_PROPERTY_OBJECT_CURRENCY_MISMATCH | 422 | INV-PRJ-6: CurrencyId объекта недвижимости ≠ Project.CurrencyId | Валюта задаётся проектом; поле readonly |
CHESSBOARD_PROPERTY_OBJECT_HIDE_FORBIDDEN | 409 | INV-CHS-009: hide не из Free (A4) | Показать текущий статус/таймер |
CHESSBOARD_PROPERTY_OBJECT_TENANT_MISMATCH / CHESSBOARD_BUILDING_TENANT_MISMATCH | 403 | Мутация чужого tenant-а | Ошибка доступа |
| — (validation) | 400 | Диапазоны/форматы (шаг проверки входа) | Подсветить поля по ошибке валидации |
| — (concurrency) | 409 | RowVersion устарел | Перечитать, повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
BuildingCreatedEvent / BuildingStatusChangedEvent | Создание / publish/hide здания | in-proc |
LayoutCreatedEvent / LayoutGroupCreatedEvent | Создание планировки/группы | in-proc |
PropertyObjectCreatedEvent | Успешное создание объекта недвижимости | in-proc |
PropertyObjectStatusChangedEvent | Hide/Unhide (A4) | in-proc → триггерит recalc |
ProjectAggregatesRecalcRequestedEvent | Создание/статус/цена объекта недвижимости | job project-aggregates-recalc (debounce 30 с) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
BuildingUpsertedKafkaEvent.v1 | pin.chessboard.building-upserted.v1 | Создание/значимое обновление/статус здания | catalog, dev-stats |
LayoutUpsertedKafkaEvent.v1 | pin.chessboard.layout-upserted.v1 | Создание/обновление/архив планировки | dev-stats, rewards, invest-analytics, catalog |
LayoutGroupUpsertedKafkaEvent.v1 | pin.chessboard.layout-group-upserted.v1 | Создание/обновление/архив группы | dev-stats, rewards, catalog |
PropertyObjectUpsertedKafkaEvent.v1 | pin.chessboard.property-object-upserted.v1 | Создание/правка паспортных полей объекта недвижимости | catalog, dev-stats, rewards |
PropertyObjectStatusChangedKafkaEvent.v1 | pin.chessboard.property-object-status-changed.v1 | Hide/Unhide объекта недвижимости (A4) | catalog, favorites, selections, notifications |
ProjectUpsertedKafkaEvent.v1 | pin.chessboard.project-upserted.v1 | Пересчёт агрегатов job-ом (шаг 5) | catalog, dev-stats, identity, news |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Объект недвижимости создаётся только в существующем здании своего tenant-а; TenantId наследуется цепочкой проект→здание→объект недвижимости | тест INV-CHS-004/005 |
| AC2 | Дубль номера в здании отклоняется и под гонкой (unique index) | параллельный тест двух созданий |
| AC3 | Floor > FloorsTotal отклоняется; уменьшение FloorsTotal ниже занятого этажа отклоняется | тест INV-BLD-3 в обе стороны |
| AC4 | PricePerMeter = round(Price/TotalArea, 2) пересчитывается при создании и правке площади/цены | unit-тест вычисления |
| AC5 | После создания объекта недвижимости в течение debounce-окна пересчитаны PropertyObjectsTotalCount/PropertyObjectsFreeCount/Price | интеграционный тест job-а |
| AC6 | Hide допустим только из Free; Unhide возвращает в Free | тест INV-CHS-009 по всем статусам |
| AC7 | Каждое создание публикует соответствующий *-upserted атомарно с мутацией | outbox-тест |
| AC8 | Объект недвижимости наследует CurrencyId проекта; передача иной валюты отклоняется | тест INV-PRJ-6 |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: POST /api/v1/developers/projects/{projectId}/buildings — Создание здания, POST /api/v1/developers/buildings/{buildingId}/status — Смена статуса здания, POST /api/v1/developers/buildings/{buildingId}/property-objects — Создание объекта недвижимости, POST /api/v1/developers/property-objects/{propertyObjectId}/hide — Скрытие объекта недвижимости, POST /api/v1/developers/property-objects/{propertyObjectId}/unhide — Возврат объекта недвижимости в продажу, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки, POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок.
- Domain entities: ../domain/building.md, ../domain/property-object.md, ../domain/layout.md, ../domain/layout-group.md, ../domain/project.md.
- Background: ../background/project-aggregates-recalc.md.
- Связанные UC: UC-CHS-001 (проект), UC-CHS-003 (цена), UC-CHS-004 (шахматка), UC-CHS-006 (статусы CRM).