Event Contract: pin.chessboard.building-upserted (BuildingUpsertedKafkaEvent)
Версионируемый контракт интеграционного события «здание создано/обновлено». Consumer-документы ссылаются
сюда, не переописывая payload. Это snapshot-событие агрегата «Здание» (литер/корпус): создание и
значимое изменение публичных полей здания. Смена только денормализованных счётчиков объектов
недвижимости (PropertyObjectsTotalCount/PropertyObjectsFreeCount — вне payload) событие не порождает.
Назначение
Фиксирует создание BuildingEntity (синк Talent, Developers API) и каждое значимое обновление публичных
полей здания (Name/Status/FloorsTotal/SectionsCount/Delivery*/ConstructionStatus/
ReadinessPercent). Событие публичное: даёт downstream-контекстам snapshot здания без синхронного запроса
в chessboard — карточка проекта и фильтр «срок сдачи» витрины (catalog), измерение Building в
статистике застройщика (dev-stats, ADR-0057).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.chessboard.building-upserted |
| C# type | BuildingUpsertedKafkaEvent |
| Topic | pin.chessboard.building-upserted.v1 |
| Partitions / key | 3 партиции (зданий — десятки тысяч, низкий объём) / key = Id — порядок per-building гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: Developers API create-building / update-building / set-building-status, синк Talent (talent-chessboard-sync), job project-aggregates-recalc (только при мутации snapshot-поля — см. «Когда публикуется») |
| Статус | draft |
Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип
агрегата-источника (AggregateId = Id, AggregateType = "Building"); сквозной trace восстанавливается из
метаданных. Публикуется атомарно со сменой состояния агрегата (транзакционный outbox).
Payload BuildingUpsertedKafkaEvent
Snapshot публичного бизнес-состояния здания. Дом полей — entity: BuildingEntity;
здесь перечислено только то, что реально в событии. Реплика-поля/PIN-поля (SyncSource), денормализованные
счётчики, аудит и concurrency в payload не входят — потребителям нужен публичный срез, а не внутреннее
устройство агрегата.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | BuildingEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | BuildingEntity.TenantId | Immutable | Tenant boundary = DeveloperId (ADR-0052). |
ProjectId | Guid | Да | BuildingEntity.ProjectId | Immutable | Проект-родитель (ключ иерархии измерений dev-stats). |
Status | string (enum BuildingStatus) | Да | BuildingEntity.Status | Additive only (новые значения enum) | Draft / Published / Hidden (building.md, stateDiagram). |
Name | string | Да | BuildingEntity.Name | Additive only | Название литера/корпуса («Литер 3», «Корпус Б»); ≤ 128, уникально в проекте (INV-BLD-2). |
FloorsTotal | short | Да | BuildingEntity.FloorsTotal | Additive only | Этажность — ось Y шахматки; 1..200. |
SectionsCount | short? | Нет | BuildingEntity.SectionsCount | Additive only | Число секций/подъездов — ось X шахматки; 1..50. |
DeliveryYear | short? | Нет | BuildingEntity.DeliveryYear | Additive only | Год сдачи; 2000..2100. Основа фильтра «срок сдачи» витрины. |
DeliveryQuarter | short? | Нет | BuildingEntity.DeliveryQuarter | Additive only | Квартал сдачи; 1..4, обязателен при заполненном DeliveryYear; отображение «IV кв. 2027». |
ConstructionStatus | string (enum ConstructionStatus) | Да | BuildingEntity.ConstructionStatus | Additive only (новые значения enum) | Planned / UnderConstruction / Commissioned / Delivered (reference-data.md); может отличаться от статуса проекта (очереди). |
ReadinessPercent | decimal | Да | BuildingEntity.ReadinessPercent | Additive only | Готовность здания, 0..100, precision (5,2). |
Version | long | Да | BuildingEntity.Version | Immutable semantics | Доменная версия агрегата; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание здания. |
Вложенных моделей нет — все поля здания атомарны. DeliveryQuarter != null ⇒ DeliveryYear != null —
потребитель может полагаться на парность при построении фильтра «срок сдачи».
Когда публикуется
Значимое изменение — мутация любого snapshot-поля бизнес-состояния. Изменение только денормализованных
счётчиков объектов недвижимости (PropertyObjectsTotalCount/PropertyObjectsFreeCount), аудита или
ReplicaVersion — не значимое, событие не порождает.
| Условие | Переход состояния | Согласованность |
|---|---|---|
| Создание здания: синк Talent или Developers API создания здания | [*] → Draft (или начальный статус синка) | публикация атомарна со сменой состояния агрегата |
Значимое обновление: Name / FloorsTotal / SectionsCount / DeliveryYear / DeliveryQuarter / ConstructionStatus / ReadinessPercent (Developers API update-building или синк Talent) | без перехода | та же |
Смена Status: Publish (INV-BLD-4) / Hide / Unhide / Unpublish (Developers API set-building-status) | Draft ↔ Published ↔ Hidden (building.md stateDiagram) | та же |
Пометка сдачи: ConstructionStatus → Delivered + ReadinessPercent = 100 (INV-BLD-6) | без перехода Status | та же |
Job project-aggregates-recalc — только если пересчёт затронул snapshot-поле; чистый пересчёт счётчиков (PropertyObjects*Count вне payload) события не публикует | — | та же |
Потребители (consumers)
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
catalog | Проекция корпуса в карточке проекта витрины (этажность, готовность, статус строительства) + денормализация min(DeliveryYear,DeliveryQuarter) проекта для фильтра/сортировки «срок сдачи»; только Published здания видимы гостю | MessageId + guard по Version | < 1 мин |
dev-stats | Реплика измерения Building (иерархия Developer→Project→Building для обогащения facts, ADR-0057) | то же | < 5 мин |
Доставка и обработка отказов
- Доставка — at-least-once. Транзакционный outbox гарантирует, что событие не теряется при коммите
агрегата, но допускает повторную доставку; порядок сообщений по ключу (
Id) сохраняется в пределах партиции. - Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченное или
устаревшее событие отбрасывается монотонным
Version-guard-ом (событие с версией ≤ применённой — skip), поэтому повторная доставка не меняет реплику. - Ретраи. Временная ошибка потребителя (недоступность его БД/зависимости) → повтор с backoff до успеха; событие не теряется, порядок per-building не нарушается.
- DLQ. Стабильно необрабатываемое сообщение («ядовитое») после исчерпания ретраев уходит в DLQ
потребителя с алертом — чтобы не блокировать партицию; расхождение реплики закрывается повторной обработкой
из DLQ либо ресинком измерения из источника (
chessboard). - Окно согласованности. Пока потребитель не догнал (SLA из таблицы «Потребители»), downstream показывает прежнее состояние здания; обновление карточки проекта и фильтра «срок сдачи» витрины видно с задержкой в пределах SLA.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса.v1. - Additive-only для minor: новые поля — optional с безопасным default, без bump
ContractVersion. - Breaking (удаление/переименование/смена типа/смысла поля) — новый
ContractVersion = 2+ новый топикpin.chessboard.building-upserted.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum (
Status,ConstructionStatus) у потребителя — fallback: warning + сохранение строки как есть; дляStatusповедение — как для наиболее ограничивающего (Hidden, не показывать публично); синк-fallbackConstructionStatus=UnderConstruction(reference-data.md, метрикаchessboard_sync_unknown_enum_total). - Порядок per-building гарантирован key-ом (
Id); потребители обязаны держатьVersion-guard. - Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Id, TenantId, ProjectId, Version | Internal | include |
Status, Name, FloorsTotal, SectionsCount, DeliveryYear, DeliveryQuarter, ConstructionStatus, ReadinessPercent | Public (юрданные/структура проектов публичны по 214-ФЗ; точный «срок сдачи» скрывается от неавторизованных потребителем — query-проекции catalog) | include |
Здание не несёт персональных данных — PII в payload отсутствует (маскирование неприменимо,
фиксируется явно). Событие НЕ содержит secrets, tokens, credentials, платёжных реквизитов, unbounded
вложений (схемы/фото хода строительства — только FileId на стороне владельца, в событие не входят).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/v1/developers/buildings/{buildingId}/status — Смена статуса здания, PUT /api/v1/developers/buildings/{buildingId} — Обновление здания
- Консюмеры (2): Consumer: DevStatsBuildingUpsertedConsumer, Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: project-aggregates-recalc (пересчёт денормализованных агрегатов), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- dimensions (1): dim_building (ClickHouse dimension)
- registries (1): Реестр событий PIN
Связанные документы
- Источник: Developers API create-building / update-building / set-building-status; talent-chessboard-sync (синк).
- Сущность: building.md (INV-BLD-1/2/4/6, stateDiagram); enum-ы:
reference-data.md (
BuildingStatus,ConstructionStatus). - Соседние события агрегатов иерархии: project-upserted.md, property-object-upserted.md.
- ADR-0052 (tenancy), ADR-0056 (simplicity-first), ADR-0057 (dev-stats потребляет измерения).