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

UC-CHS-002. Добавление здания (литера/корпуса) и объектов проекта застройщиком

Назначение

Менеджер застройщика наполняет проект структурой: добавляет здание (литер/корпус: этажность, секции, срок сдачи, готовность), при необходимости заводит планировки и группы планировок, затем создаёт объекты недвижимости (номер, этаж, секция, комнатность, площади, цена, вид из окна, вместимость) — будущие ячейки шахматки. Сценарий относится к контексту chessboard.

Metadata

ПолеЗначение
IDUC-CHS-002
Контекстchessboard (см. docs/06-services/chessboard/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольДевелопер (менеджер застройщика)
Permissions / scopechessboard.buildings.manage + chessboard.property-objects.manage + chessboard.layouts.manage; scope = организация / группа проектов / проект
TenantTenantId = CompanyId застройщика (ADR-0052); наследуется дочерними сущностями от проекта (INV-BLD-1, INV-CHS-004, INV-LAY-1, INV-LGR-1)
Record-levelНе применяется

Предусловие

  • Проект существует и принадлежит tenant-у актора (UC-CHS-001); статус проекта — любой не-Archived (структуру можно наполнять и в Draft).
  • Для привязки объекта недвижимости к планировке — планировка того же проекта заведена (этим же сценарием, шаг 4).

Триггер

ПолеЗначение
ИсточникДействие пользователя в кабинете застройщика
Триггерящий элементКабинет застройщика → карточка проекта → «Добавить корпус» / «Добавить планировку» / «Добавить объект»

Основной Поток

Добавление и публикация здания

  1. Менеджер создаёт здание (POST /developers/projects/{projectId}/buildings): Name («Литер 3»), FloorsTotal, SectionsCount, DeliveryYear+DeliveryQuarter, ConstructionStatus, ReadinessPercent, CeilingHeightMeters.
    • Вход: Name 1..128; FloorsTotal 1..200; SectionsCount 1..50; DeliveryYear 2000..2100, DeliveryQuarter 1..4 (обязателен при годе); ReadinessPercent 0..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.
  2. Менеджер публикует здание (POST /developers/buildings/{id}/publish): guard INV-BLD-4 — Project.Status in (Draft, Published), заполнены FloorsTotal, DeliveryYear+DeliveryQuarter; иначе CHESSBOARD_BUILDING_PUBLISH_INCOMPLETE. Переход Draft → Published, публикуется BuildingUpsertedKafkaEvent.

Планировки и группы (опционально)

  1. Менеджер заводит группу планировок (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.

Добавление объекта недвижимости

  1. Менеджер создаёт объект в корпусе (POST /developers/buildings/{buildingId}/property-objects): Number, Floor, Section, Rooms, TotalArea, LivingArea, Price, LayoutId, WindowViews, Capacity (для отелей), CeilingHeightMeters, Description.
    • Вход: Number 1..32 и уникален в здании по нижнему регистру (INV-CHS-006); Floor −5..200 и Floor <= Building.FloorsTotal (INV-BLD-3); Section ≤ Building.SectionsCount; Rooms 0..20; TotalArea > 0; LivingArea <= TotalArea; Price >= 0; Capacity 1..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.

Пересчёт денормализаций

  1. Фоновый job project-aggregates-recalc (debounce 30 с по ProjectAggregatesRecalcRequestedEvent) пересчитывает Project.Price/PropertyObjectsTotalCount/PropertyObjectsFreeCount, Building.PropertyObjects*Count, Layout.PropertyObjectsCount, LayoutGroup.LayoutsCount/Area; изменение агрегатов проекта публикует ProjectUpsertedKafkaEvent с новым диапазоном цен.
  2. Шахматка проекта (UC-CHS-004) получает новую ячейку Floor × Section; фронт обновляет дерево «корпуса → объекты».

Альтернативные Потоки

A1. Дубль номера объекта в здании

  1. Номер уже занят в здании (или гонку поймал unique-индекс ux_property_objects_building_number) → CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN (INV-CHS-006). Менеджер меняет номер.

A2. Этаж выше этажности здания

  1. Floor > Building.FloorsTotalCHESSBOARD_BUILDING_FLOORS_CONFLICT (INV-BLD-3).
  2. Обратная сторона: попытка уменьшить FloorsTotal здания ниже максимального занятого этажа отклоняется тем же кодом.

A3. Планировка чужого проекта

  1. layout.ProjectId != building.ProjectIdCHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH (INV-CHS-007); аналогично для группы: CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH (INV-LAY-3).

A4. Скрытие/показ объекта недвижимости застройщиком

  1. POST /developers/property-objects/{id}/hide: guard INV-CHS-009 — скрыть можно только из Free (Reserved/Booked/Sold скрыть нельзя → CHESSBOARD_PROPERTY_OBJECT_HIDE_FORBIDDEN); переход Free → Hidden, StatusSource = DeveloperApi, публикуется PropertyObjectStatusChangedKafkaEvent.
  2. POST .../unhideHidden → Free, тем же путём. Это единственные статусные переходы, доступные Developers API (INV-CHS-001).

A5. Правка паспортных полей объекта недвижимости-реплики Talent

  1. Для SyncSource = TalentChessboard реплика-поля (Number, Floor, Section, Rooms, площади, WindowViews, LayoutId) правятся только синком; их правка через Developers API отклоняется (аналог INV-PRJ-9 для объекта недвижимости). PIN-поля Description, Price — доступны.

A6. Здание/объект недвижимости в скрытом проекте

  1. Наполнение структуры допустимо при 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-upsertedcatalog рисует новую зелёную ячейку Floor × Section в пределах SLA (< 1 мин). Денормализации Project.Price/счётчики, Building.PropertyObjects*Count, Layout.PropertyObjectsCount, LayoutGroup.LayoutsCount/Area пересчитывает job project-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_TAKEN409INV-BLD-2: имя корпуса занято в проектеПредложить другое имя
CHESSBOARD_BUILDING_PUBLISH_INCOMPLETE422INV-BLD-4: нет этажности/срока сдачиПодсветить поля
CHESSBOARD_BUILDING_FLOORS_CONFLICT422INV-BLD-3: этаж объекта недвижимости вне этажности (A2)Поправить этаж/этажность
CHESSBOARD_PROPERTY_OBJECT_NUMBER_TAKEN409INV-CHS-006 (A1)Поправить номер
CHESSBOARD_PROPERTY_OBJECT_BUILDING_PROJECT_MISMATCH422INV-CHS-005: здание другого проектаОбновить форму (некорректный buildingId)
CHESSBOARD_PROPERTY_OBJECT_LAYOUT_PROJECT_MISMATCH422INV-CHS-007 (A3)Выбрать планировку своего проекта
CHESSBOARD_PROPERTY_OBJECT_CURRENCY_MISMATCH422INV-PRJ-6: CurrencyId объекта недвижимости ≠ Project.CurrencyIdВалюта задаётся проектом; поле readonly
CHESSBOARD_PROPERTY_OBJECT_HIDE_FORBIDDEN409INV-CHS-009: hide не из Free (A4)Показать текущий статус/таймер
CHESSBOARD_PROPERTY_OBJECT_TENANT_MISMATCH / CHESSBOARD_BUILDING_TENANT_MISMATCH403Мутация чужого tenant-аОшибка доступа
— (validation)400Диапазоны/форматы (шаг проверки входа)Подсветить поля по ошибке валидации
— (concurrency)409RowVersion устарелПеречитать, повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
BuildingCreatedEvent / BuildingStatusChangedEventСоздание / publish/hide зданияin-proc
LayoutCreatedEvent / LayoutGroupCreatedEventСоздание планировки/группыin-proc
PropertyObjectCreatedEventУспешное создание объекта недвижимостиin-proc
PropertyObjectStatusChangedEventHide/Unhide (A4)in-proc → триггерит recalc
ProjectAggregatesRecalcRequestedEventСоздание/статус/цена объекта недвижимостиjob project-aggregates-recalc (debounce 30 с)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
BuildingUpsertedKafkaEvent.v1pin.chessboard.building-upserted.v1Создание/значимое обновление/статус зданияcatalog, dev-stats
LayoutUpsertedKafkaEvent.v1pin.chessboard.layout-upserted.v1Создание/обновление/архив планировкиdev-stats, rewards, invest-analytics, catalog
LayoutGroupUpsertedKafkaEvent.v1pin.chessboard.layout-group-upserted.v1Создание/обновление/архив группыdev-stats, rewards, catalog
PropertyObjectUpsertedKafkaEvent.v1pin.chessboard.property-object-upserted.v1Создание/правка паспортных полей объекта недвижимостиcatalog, dev-stats, rewards
PropertyObjectStatusChangedKafkaEvent.v1pin.chessboard.property-object-status-changed.v1Hide/Unhide объекта недвижимости (A4)catalog, favorites, selections, notifications
ProjectUpsertedKafkaEvent.v1pin.chessboard.project-upserted.v1Пересчёт агрегатов job-ом (шаг 5)catalog, dev-stats, identity, news

Acceptance Criteria

#КритерийПроверка
AC1Объект недвижимости создаётся только в существующем здании своего tenant-а; TenantId наследуется цепочкой проект→здание→объект недвижимоститест INV-CHS-004/005
AC2Дубль номера в здании отклоняется и под гонкой (unique index)параллельный тест двух созданий
AC3Floor > FloorsTotal отклоняется; уменьшение FloorsTotal ниже занятого этажа отклоняетсятест INV-BLD-3 в обе стороны
AC4PricePerMeter = round(Price/TotalArea, 2) пересчитывается при создании и правке площади/ценыunit-тест вычисления
AC5После создания объекта недвижимости в течение debounce-окна пересчитаны PropertyObjectsTotalCount/PropertyObjectsFreeCount/Priceинтеграционный тест job-а
AC6Hide допустим только из Free; Unhide возвращает в Freeтест INV-CHS-009 по всем статусам
AC7Каждое создание публикует соответствующий *-upserted атомарно с мутациейoutbox-тест
AC8Объект недвижимости наследует CurrencyId проекта; передача иной валюты отклоняетсятест INV-PRJ-6

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

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

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