Event Contract: pin.chessboard.property-object-upserted (PropertyObjectUpsertedKafkaEvent)
Версионируемый контракт интеграционного события «объект недвижимости создан/обновлён». Consumer-документы ссылаются сюда, не переописывая payload. Это «property-object-updated»-событие шахматки: паспортный snapshot объекта недвижимости; смена только статуса и только цены — отдельные специализированные события того же агрегата (property-object-status-changed, property-object-price-changed), чтобы высокочастотные потребители (витрина, аналитика цен) не разбирали полный snapshot.
Назначение
Фиксирует создание PropertyObjectEntity (синк Talent, Developers API) и каждое обновление паспортных полей
(Number/Floor/Section/Rooms/площади/LayoutId/WindowViews/Capacity). Событие публичное:
даёт downstream-контекстам полный snapshot объекта недвижимости без синхронного запроса в chessboard —
карточка и списки объектов витрины, измерение PropertyObject статистики застройщика (ADR-0057), реестр
объектов недвижимости для правил вознаграждений и эконом-модели.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.chessboard.property-object-upserted |
| C# type | PropertyObjectUpsertedKafkaEvent |
| Topic | pin.chessboard.property-object-upserted.v1 |
| Partitions / key | 6 партиций (объекты недвижимости — самый массовый агрегат шахматки, сотни тысяч) / key = PropertyObjectId — порядок событий по объекту недвижимости гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: Developers API управления объектами недвижимости, синк Talent (talent-chessboard-sync) |
| Статус | draft |
Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип
агрегата-источника (AggregateId = PropertyObjectId, AggregateType = "PropertyObject"); сквозной trace
восстанавливается из метаданных. Публикуется атомарно со сменой состояния агрегата (транзакционный outbox).
Payload PropertyObjectUpsertedKafkaEvent
Полный паспортный snapshot. Дом полей — entity: PropertyObjectEntity; здесь
перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | PropertyObjectEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | PropertyObjectEntity.TenantId | Immutable | Tenant boundary = DeveloperId (ADR-0052). |
ProjectId | Guid | Да | PropertyObjectEntity.ProjectId | Immutable | Проект (ключ иерархии измерений dev-stats). |
BuildingId | Guid | Да | PropertyObjectEntity.BuildingId | Immutable | Здание/литер. |
LayoutId | Guid? | Нет | PropertyObjectEntity.LayoutId | Additive only | Планировка объекта недвижимости. |
LayoutGroupId | Guid? | Нет | Layout.LayoutGroupId (через привязанную планировку; null, если планировки/группы нет) | Additive only | Группа планировок — денормализация в payload, чтобы dev-stats не джойнил измерения при вставке facts (ADR-0057). |
Status | string (enum PropertyObjectStatus) | Да | PropertyObjectEntity.Status | Additive only (новые значения enum) | Free / Reserved / Booked / Sold / Hidden — текущий статус на момент snapshot-а; переходы отслеживаются событием property-object-status-changed. |
Number | string | Да | PropertyObjectEntity.Number | Additive only | Номер объекта (≤ 32, уникален в здании). |
Floor | short | Да | PropertyObjectEntity.Floor | Additive only | Этаж (ось Y шахматки). |
Section | short? | Нет | PropertyObjectEntity.Section | Additive only | Секция/подъезд (ось X шахматки). |
Rooms | short | Да | PropertyObjectEntity.Rooms | Additive only | Комнатность; 0 = студия. |
TotalArea | decimal | Да | PropertyObjectEntity.TotalArea | Additive only | Общая площадь, м². |
LivingArea | decimal? | Нет | PropertyObjectEntity.LivingArea | Additive only | Жилая площадь, м². |
Price | decimal | Да | PropertyObjectEntity.Price | Additive only | Текущая цена на момент snapshot-а; изменения цены отслеживаются событием property-object-price-changed. |
CurrencyId | short | Да | PropertyObjectEntity.CurrencyId | Additive only | Валюта цены — ссылка на справочник валют (= Project.CurrencyId, INV-PRJ-6; RUB-only, ADR-0048). |
CurrencySnapshot | CurrencySnapshot | Да | реплика справочника на момент публикации | Additive only | Display-снапшот валюты (IsoCode/Name/Sign — snapshots.md); отдельного строкового кода валюты payload не несёт. |
PricePerMeter | decimal | Да | PropertyObjectEntity.PricePerMeter | Additive only | Цена за м² (вычисляемая денормализация агрегата). |
WindowViews | IReadOnlyList<string> (enum WindowViewKind) | Да (может быть пустым) | PropertyObjectEntity.WindowViews.Views | Additive only | Виды из окон: Sea/City/Courtyard/Mountains/Park/River/Street (reference-data.md); ≤ 4, без дублей. |
Capacity | short? | Нет | PropertyObjectEntity.Capacity | Additive only | Вместимость (гостей) — апарт-отели. |
Version | long | Да | PropertyObjectEntity.Version | Immutable semantics | Доменная версия агрегата; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание объекта недвижимости. |
CreatedAt | DateTimeOffset | Да | PropertyObjectEntity.CreatedAt | Immutable | Момент публикации/создания объекта недвижимости; неизменен между snapshot-ами. Потребитель dev-stats реплицирует в dim_property_object.PublishedAt для метрики time-to-first-view. |
Единственная вложенная модель — CurrencySnapshot: дом полей —
snapshots.md, здесь не переописывается; снапшот фиксируется на момент
публикации события и не «догоняется». Коллекция WindowViews плоская (enum-строки).
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
| Создание объекта недвижимости: синк Talent или Developers API создания объекта недвижимости | [*] → Free (или начальный статус синка) | публикация атомарна со сменой состояния агрегата |
Обновление паспортных полей: Number / Floor / Section / Rooms / TotalArea / LivingArea / LayoutId / WindowViews / Capacity (синк или Developers API) | без перехода | та же |
Не публикуется при изменении только Status (→ property-object-status-changed) или только Price (→ property-object-price-changed) | — | — |
Потребители (consumers)
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
catalog | Проекция ApartmentListItem/ApartmentDetail витрины: паспортные поля, привязка планировки | MessageId + guard по Version | < 1 мин |
dev-stats | Реплика измерения PropertyObject (иерархия Developer→Project→Building→LayoutGroup→Layout→PropertyObject для обогащения facts, ADR-0057) | то же | < 5 мин |
rewards | Реестр объектов недвижимости для привязки правил вознаграждений (через LayoutId/LayoutGroupId) | то же | < 5 мин |
invest-analytics | Реестр объектов недвижимости/типов (LayoutId) для эконом-модели и расчёта доходности | то же | < 5 мин |
Доставка и обработка отказов
- Доставка — at-least-once. Транзакционный outbox гарантирует, что событие не теряется при коммите
агрегата, но допускает повторную доставку; порядок сообщений по ключу (
PropertyObjectId) сохраняется в пределах партиции. - Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченное или
устаревшее событие отбрасывается монотонным
Version-guard-ом (событие с версией ≤ применённой — skip). - Ретраи. Временная ошибка потребителя → повтор с backoff до успеха; событие не теряется, порядок per-object не нарушается.
- DLQ. Стабильно необрабатываемое сообщение после исчерпания ретраев уходит в DLQ потребителя с алертом —
чтобы не блокировать партицию; расхождение реплики закрывается повторной обработкой из DLQ либо ресинком
измерения из источника (
chessboard).
property-object-upserted / -status-changed / -price-changed — разные топики; относительный порядок
между ними не гарантирован. Потребитель, ведущий единую реплику объекта недвижимости, держит общий
Version-guard по PropertyObjectId и применяет только события с большей версией (паспортное поле, статус
и цена меняют одну доменную версию агрегата).
Пока потребитель не догнал (SLA из таблицы «Потребители»), проекция объекта недвижимости витрины и измерения
показывают прежний паспортный snapshot; обновление видно с задержкой в пределах SLA (< 1 мин для catalog).
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии события — warn-and-proceed (потребитель логирует warning и продолжает). Топик кодирует версию конвенцией суффикса.v1. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа/смысла поля) — новый
ContractVersion = 2+ новый топикpin.chessboard.property-object-upserted.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum (
Status, элементыWindowViews) у потребителя — fallback: warning + сохранение строки как есть; дляStatusповедение — как для наиболее ограничивающего (Hidden, не показывать). CurrencySnapshotверсионируется вместе с несущим контрактом (отдельногоContractVersionнет); изменения snapshot-модели — только additive, неизвестные поля снапшота потребитель игнорирует (snapshots.md).- Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Все идентификаторы, Version | Internal | include |
Status, Number, Floor, Section, Rooms, TotalArea, LivingArea, WindowViews, Capacity | Public | include |
Price, CurrencyId + CurrencySnapshot, PricePerMeter | Public (в публичных проекциях скрывается от неавторизованных потребителем — правило query-проекций property-object.md) | include |
Событие НЕ содержит secrets, tokens, PII (участники сделок — контекст crm), unbounded вложений
(схемы планировок — событие layout-upserted).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): GET /api/v1/dev-stats/time-to-first-view — Время до первого просмотра (TTFV), POST /api/v1/developers/buildings/{buildingId}/property-objects — Создание объекта недвижимости, POST /api/v1/developers/buildings/{buildingId}/property-objects/bulk — Массовый upsert объектов недвижимости, POST /api/v1/developers/property-objects/{propertyObjectId}/price — Изменение цены объекта недвижимости, PUT /api/v1/developers/property-objects/{propertyObjectId} — Обновление объекта недвижимости (паспортные поля)
- События (6): Event Contract: pin.chessboard.building-upserted (BuildingUpsertedKafkaEvent), Event Contract: pin.chessboard.developer-upserted (DeveloperUpsertedKafkaEvent), Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent), Event Contract: pin.chessboard.project-upserted (ProjectUpsertedKafkaEvent), Event Contract: pin.chessboard.property-object-price-changed (PropertyObjectPriceChangedKafkaEvent), Event Contract: pin.chessboard.property-object-status-changed (PropertyObjectStatusChangedKafkaEvent)
- Консюмеры (2): Consumer: DevStatsPropertyObjectUpsertedConsumer, Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- dimensions (1): dim_property_object (ClickHouse dimension)
- registries (1): Реестр событий PIN
Связанные документы
- Источник: talent-chessboard-sync (синк) и Developers API управления объектами недвижимости.
- Сущность: property-object.md; соседние события агрегата: property-object-status-changed.md, property-object-price-changed.md.
- Snapshot-модель валюты: snapshots.md; реплика справочников — handbook-replica.md.
- ADR-0052, ADR-0057, ADR-0063 (справочники), ADR-0048 (RUB-only).